Le langage Swift permet une manipulation élégante et concise des propriétés d'objets grâce à l'utilisation de key paths. Ces derniers offrent une méthode sûre et flexible pour accéder aux propriétés sans recourir à des chaînes de caractères, évitant ainsi les erreurs typographiques. Ce mécanisme peut être combiné avec d'autres fonctionnalités puissantes de Swift, comme la fonction map ou encore filter, afin de rendre le code plus lisible et modulaire. Prenons un exemple simple pour illustrer ce concept.
Lorsqu'on souhaite extraire un ensemble de valeurs d'une collection d'objets, l'utilisation de key paths dans la fonction map permet d'écrire un code plus succinct. Par exemple, pour récupérer un tableau de noms à partir d'une liste de personnes, on peut utiliser le code suivant :
Ce code permet d'extraire directement la propriété name de chaque élément de la collection people grâce au key path \.name. Une autre manière de réaliser cette opération serait d'utiliser une fermeture, ce qui donne un code équivalent, mais légèrement plus verbeux :
Bien que les deux exemples donnent le même résultat, l'utilisation des key paths est clairement plus concise et offre une meilleure lisibilité du code. Cependant, dans certains cas, cette approche peut perdre de son efficacité, notamment lorsqu'il s'agit d'utiliser la fonction filter.
Dans le cas de la fonction filter, qui permet de filtrer une collection selon un critère spécifique, l'utilisation des key paths peut rendre le code plus complexe et moins intuitif. Prenons l'exemple suivant où l'on souhaite obtenir une liste de personnes âgées de 18 ans ou plus :
Ici, on utilise le key path \.age pour accéder à la propriété age de chaque élément de la collection. Toutefois, il est possible de réécrire ce code de manière plus simple, sans recourir aux key paths :
Bien que l'utilisation des key paths puisse rendre le code plus lisible dans certains cas (comme avec map), il est important de peser les avantages et les inconvénients lorsqu'on l'utilise avec filter. Les key paths sont des outils puissants, mais il est essentiel de les utiliser judicieusement pour éviter de rendre le code plus compliqué qu'il ne devrait l'être.
Un autre aspect intéressant de Swift est l'utilisation combinée des key paths et de l'annotation @dynamicMemberLookup. Cette annotation permet de rendre l'accès aux propriétés des objets encore plus flexible, en permettant de traiter des propriétés comme si elles faisaient directement partie de l'objet, même lorsqu'elles sont imbriquées dans des structures complexes.
Prenons l'exemple de deux structures qui définissent un profil d'utilisateur, comprenant une adresse et des informations personnelles. Grâce à l'annotation @dynamicMemberLookup, il devient possible d'accéder aux propriétés de manière transparente, sans avoir à spécifier l'objet parent à chaque fois.
Voici comment on pourrait définir ces structures et accéder aux propriétés via un profil utilisateur dynamique :
Avec cette approche, il devient possible d'accéder aux propriétés des structures User et Address de manière fluide et directe :
Dans cet exemple, grâce à l'utilisation combinée de @dynamicMemberLookup et des key paths, on peut accéder aux propriétés imbriquées sans avoir à mentionner explicitement l'objet address à chaque fois. Cela simplifie considérablement le code et améliore sa lisibilité.
Cette technique est particulièrement utile lorsque l'on travaille avec des objets complexes ou des données structurées. Cependant, bien que cette fonctionnalité puisse rendre le code plus élégant et concis, elle comporte aussi des risques potentiels. Il est crucial de valider les clés dynamiques utilisées, car des erreurs dans les noms de propriétés ne déclencheront pas d'avertissements du compilateur, rendant le code susceptible de comporter des erreurs difficiles à détecter.
En résumé, les key paths et l'annotation @dynamicMemberLookup offrent des moyens puissants de rendre le code Swift plus lisible et flexible. Cependant, ces outils doivent être utilisés avec précaution pour éviter que la simplicité apparente ne cache des erreurs subtiles. En comprenant comment et quand utiliser ces fonctionnalités, vous pouvez améliorer la qualité de votre code tout en conservant sa clarté et sa maintenabilité.
Comment gérer la concurrence et la parallélisation dans les systèmes modernes ?
À l'époque où tous les processeurs étaient mono-cœurs, la seule façon de faire exécuter plusieurs tâches simultanément à un système était d'avoir plusieurs processeurs. Cela nécessitait également un logiciel spécialisé pour tirer parti de ces processeurs multiples. Aujourd'hui, presque tous les appareils sont équipés de processeurs multi-cœurs, et les systèmes d'exploitation tels qu'iOS et macOS sont conçus pour tirer parti de ces cœurs multiples afin d'exécuter les tâches simultanément.
Traditionnellement, les applications géraient la concurrence en créant plusieurs threads. Cependant, ce modèle ne se prête pas bien à un nombre arbitraire de cœurs. Le principal problème avec l'utilisation de threads est que nos applications peuvent fonctionner sur une grande variété de systèmes et de processeurs. Ainsi, pour optimiser notre code, nous devons savoir combien de cœurs ou de processeurs peuvent être utilisés de manière efficace à un moment donné, ce qui est généralement difficile à déterminer lors du développement.
Pour résoudre ce problème, de nombreux systèmes d'exploitation, dont iOS et macOS, ont commencé à s'appuyer sur des fonctions asynchrones. Ces fonctions sont utilisées pour initier des tâches qui peuvent prendre du temps, telles que des requêtes HTTP ou l'écriture de données sur le disque. Une fonction asynchrone démarre généralement une tâche longue et renvoie immédiatement le contrôle avant que la tâche ne soit terminée. Cette tâche s'exécute en arrière-plan et utilise une fonction de rappel (comme une closure en Swift) lorsqu'elle est terminée.
Cependant, que faire lorsque nous devons créer nos propres fonctions asynchrones sans vouloir gérer les threads nous-mêmes ? C'est ici qu'Apple propose des solutions pratiques pour gérer la concurrence et la parallélisation avec Swift, notamment à travers l'utilisation de GCD (Grand Central Dispatch).
GCD permet de gérer des files d'attente de dispatch pour exécuter des tâches soumises à ces files. Les tâches sont exécutées dans un ordre FIFO (First-In, First-Out), ce qui signifie qu'elles démarrent dans l'ordre dans lequel elles ont été soumises. Une tâche est toute opération qu'une application doit accomplir, comme effectuer des calculs, lire ou écrire des données, faire une requête HTTP, ou exécuter toute autre tâche nécessaire. Ces tâches sont définies par du code placé dans une fonction ou une closure et ajoutées à une file d'attente.
GCD propose trois types de files d'attente :
-
Files d'attente sérielles : Les tâches dans une file d'attente série (également appelée file d'attente privée) sont exécutées une par une, dans l'ordre où elles ont été soumises. Chaque tâche démarre seulement après la fin de la tâche précédente. Les files sérielles sont souvent utilisées pour synchroniser l'accès à des ressources spécifiques, car elles garantissent qu'aucune tâche ne s'exécutera simultanément. Cela permet d'éviter des conflits d'accès aux ressources partagées.
-
Files d'attente concurrentes : Les tâches dans une file d'attente concurrente (également appelée file d'attente de dispatch globale) sont exécutées simultanément, mais elles commencent dans l'ordre dans lequel elles ont été ajoutées à la file. Le nombre de tâches pouvant être exécutées en même temps varie en fonction des ressources disponibles et des conditions du système. La gestion du moment où chaque tâche commence est décidée par GCD et n'est pas sous le contrôle de l'application.
-
File d'attente principale : La file d'attente principale est une file d'attente sérielle qui exécute des tâches sur le thread principal de l'application. Puisque les tâches dans la file d'attente principale s'exécutent sur le thread principal, elles sont généralement utilisées pour mettre à jour l'interface utilisateur après que le traitement en arrière-plan soit terminé.
L'un des principaux avantages des files d'attente par rapport aux threads traditionnels est que la gestion des threads est effectuée par le système, et non par l'application. Cela permet au système de gérer dynamiquement le nombre de threads en fonction des ressources disponibles, offrant ainsi une gestion plus efficace des threads que si l'application devait le faire manuellement. De plus, les files d'attente permettent de contrôler l'ordre d'exécution des tâches. Avec les files sérielles, nous gérons non seulement l'ordre d'exécution, mais nous garantissons qu'une nouvelle tâche ne commencera pas tant que la tâche précédente ne sera pas terminée. Implementer cette logique avec des threads traditionnels peut être complexe et fragile, mais avec les files d'attente de dispatch, cette gestion devient beaucoup plus simple.
Lorsque nous avons besoin de créer une file d'attente, nous utilisons l'initialiseur DispatchQueue. Cet initialiseur prend deux paramètres principaux : un label, qui est une chaîne de caractères permettant d'identifier de manière unique la file d'attente, et les attributs, qui peuvent être .serial pour une file sérielle ou .concurrent pour une file concurrente. Par défaut, une file d'attente est sérielle.
Prenons un exemple où nous devons créer et utiliser une file d'attente concurrente. Voici un extrait de code en Swift pour créer une file d'attente concurrente et exécuter une tâche qui effectue des calculs répétitifs :
Le premier ligne crée une nouvelle file d'attente concurrente, tandis que la seconde crée une closure qui définit la tâche à accomplir. Cette tâche appelle la fonction performCalculation() et effectue des itérations sur une autre fonction de calcul.
Il est important de noter que GCD n'offre pas seulement une abstraction pratique pour la gestion de la concurrence, mais il permet également d'optimiser les performances en permettant au système d'adapter dynamiquement le nombre de threads en fonction des ressources disponibles. Cela permet de réduire les risques de surcharge du processeur et d'améliorer la réactivité des applications, surtout lorsque celles-ci traitent un grand nombre de tâches en parallèle.
Le choix entre utiliser des files d'attente sérielles ou concurrentes dépend des exigences spécifiques du programme. Une bonne pratique consiste à utiliser des files d'attente sérielles pour les tâches qui nécessitent un ordre strict d'exécution, et des files d'attente concurrentes pour les tâches indépendantes qui peuvent être traitées en parallèle.
Comment éviter les pièges courants de la programmation orientée objet dans Swift ?
Dans le cadre du développement d'un jeu vidéo, nous avons exploré l'implémentation d'un système de véhicules utilisant les principes de la programmation orientée objet (POO). Cette approche présente plusieurs avantages, comme la réutilisation du code et une organisation claire des fonctionnalités. Toutefois, elle n’est pas sans défis, et certains de ces défis sont illustrés à travers un exemple concret de code.
Prenons d'abord un petit extrait de code d’un véhicule amphibie qui contient des erreurs logiques assez simples mais révélatrices. Le tableau vehicleMovementTypes contient à tort le type .sea au lieu du type .land, ce qui peut entraîner des comportements incorrects dans le jeu, comme des véhicules amphibies se déplaçant uniquement sur l'eau alors qu'ils devraient pouvoir se déplacer à la fois sur terre et sur mer. Ce type d’erreur est relativement facile à commettre, surtout lorsqu'on travaille avec des classes et des héritages.
Une autre difficulté que la POO engendre réside dans la gestion des constantes entre les classes. En POO, il est souvent souhaité de définir des valeurs constantes dans les superclasses que les sous-classes ne peuvent pas modifier. Cependant, Swift ne permet pas de définir des constantes dans une classe parente qui peuvent ensuite être modifiées dans ses sous-classes. Ainsi, même si certaines propriétés de nos véhicules devraient être initialisées une seule fois, elles risquent de devenir des variables modifiables, ce qui perturbe le comportement attendu du programme.
En outre, un problème majeur avec l’approche orientée objet réside dans l’accès aux méthodes et aux propriétés des classes. La restriction de l’accès à certaines parties du code ne peut être effectuée qu’au niveau du fichier source. Le contrôle d’accès fileprivate permet de limiter l’accès à certaines parties du code uniquement au sein du même fichier, mais cela n’est pas toujours pratique. Si les sous-classes doivent être placées dans un fichier séparé, les contrôles d’accès doivent être modifiés, ce qui peut entraîner des fuites d’informations vers d’autres parties du projet.
En résumé, bien que la POO permette d’organiser clairement les différentes fonctionnalités d’un projet à travers des objets qui contiennent à la fois des attributs et des méthodes, elle n’est pas sans limitations. Ces limitations peuvent entraîner des hiérarchies de classes trop complexes, un code difficile à maintenir et des erreurs comme celles mentionnées dans l'exemple du véhicule amphibie. La gestion de l’héritage unique et l’absence de flexibilité dans la déclaration de constantes sont des points faibles notables. Ce chapitre introduit ces défis pour mieux préparer le terrain à une alternative potentielle : la programmation orientée protocoles (POP).
Il est crucial, dans ce contexte, de comprendre qu'en dépit de ses inconvénients, la POO reste un paradigme central dans le développement logiciel. Néanmoins, la gestion des classes et des héritages doit être pensée de manière à éviter les pièges évoqués. En particulier, il est important de structurer les hiérarchies de manière judicieuse, de ne pas surcharger les superclasses avec des fonctionnalités inutiles pour certaines sous-classes, et de gérer correctement l’accès aux données afin d’éviter les fuites d’informations.
La programmation orientée protocoles (POP), que nous aborderons dans le chapitre suivant, propose une alternative qui permet d'éviter certaines de ces limitations. En POP, l’idée est de se concentrer sur ce que fait un objet plutôt que sur ce qu’il est, ce qui ouvre la voie à un code plus flexible, réutilisable et modulaire. Les protocoles permettent de définir des comportements communs sans avoir à créer des hiérarchies complexes de classes.

Deutsch
Francais
Nederlands
Svenska
Norsk
Dansk
Suomi
Espanol
Italiano
Portugues
Magyar
Polski
Cestina
Русский