Les erreurs que la plupart des développeurs Swift ne savent pas qu'ils font

Publié: 2022-03-11

Venant d'un milieu Objective-C, au début, j'avais l'impression que Swift me retenait. Swift ne me permettait pas de progresser à cause de sa nature fortement typée, qui était parfois exaspérante.

Contrairement à Objective-C, Swift applique de nombreuses exigences au moment de la compilation. Les choses qui sont assouplies dans Objective-C, telles que le type d' id et les conversions implicites, ne sont pas une chose dans Swift. Même si vous avez un Int et un Double , et que vous voulez les additionner, vous devrez les convertir explicitement en un seul type.

De plus, les options sont une partie fondamentale du langage, et même s'il s'agit d'un concept simple, il faut un certain temps pour s'y habituer.

Au début, vous voudrez peut-être forcer tout déballer, mais cela finira par entraîner des plantages. Au fur et à mesure que vous vous familiarisez avec le langage, vous commencez à aimer le peu d'erreurs d'exécution, car de nombreuses erreurs sont détectées au moment de la compilation.

La plupart des programmeurs Swift ont une expérience antérieure significative avec Objective-C, ce qui, entre autres, pourrait les amener à écrire du code Swift en utilisant les mêmes pratiques qu'ils connaissent dans d'autres langages. Et cela peut provoquer de mauvaises erreurs.

Dans cet article, nous décrivons les erreurs les plus courantes commises par les développeurs Swift et les moyens de les éviter.

Ne vous méprenez pas - les meilleures pratiques Objective-C ne sont pas les meilleures pratiques Swift.
Tweeter

1. Options de déballage forcé

Une variable d'un type optionnel (par exemple String? ) peut contenir ou non une valeur. Lorsqu'ils ne contiennent pas de valeur, ils sont égaux à nil . Pour obtenir la valeur d'un optionnel, vous devez d'abord les déballer , et cela peut être fait de deux manières différentes.

Une façon est la liaison facultative en utilisant un if let ou un guard let , c'est-à-dire:

 var optionalString: String? //... if let s = optionalString { // if optionalString is not nil, the test evaluates to // true and s now contains the value of optionalString } else { // otherwise optionalString is nil and the if condition evaluates to false }

La seconde consiste à forcer le déballage à l'aide de la commande ! opérateur, ou en utilisant un type optionnel implicitement déballé (par exemple String! ). Si l'option est nil , forcer un déballage provoquera une erreur d'exécution et mettra fin à l'application. De plus, tenter d'accéder à la valeur d'une option implicitement déballée entraînera la même chose.

Nous avons parfois des variables que nous ne pouvons pas (ou ne voulons pas) initialiser dans l'initialiseur de classe/structure. Ainsi, nous devons les déclarer en option. Dans certains cas, nous supposons qu'ils ne seront pas nil dans certaines parties de notre code, nous les forçons donc à les déballer ou les déclarons comme des options implicitement déballées, car c'est plus facile que d'avoir à faire des liaisons facultatives tout le temps. Cela doit être fait avec soin.

Cela revient à travailler avec les IBOutlet s, qui sont des variables qui référencent un objet dans une plume ou un storyboard. Ils ne seront pas initialisés lors de l'initialisation de l'objet parent (généralement un contrôleur de vue ou un UIView personnalisé), mais nous pouvons être sûrs qu'ils ne seront pas nil lorsque viewDidLoad (dans un contrôleur de vue) ou awakeFromNib (dans une vue) est appelé, et ainsi nous pouvons y accéder en toute sécurité.

En général, la meilleure pratique consiste à éviter de forcer le déballage et d'utiliser des options implicitement déballées. Considérez toujours que l'option pourrait être nil et gérez-la de manière appropriée, soit en utilisant une liaison optionnelle, soit en vérifiant si elle n'est pas nil avant de forcer un déballage, ou en accédant à la variable en cas d'option implicitement déballée.

2. Ne pas connaître les pièges des cycles de référence solides

Un cycle de référence fort existe lorsqu'une paire d'objets conserve une référence forte l'un à l'autre. Ce n'est pas quelque chose de nouveau pour Swift, car Objective-C a le même problème, et les développeurs Objective-C chevronnés devraient gérer cela correctement. Il est important de prêter attention aux références fortes et à ce qui fait référence à quoi. La documentation Swift a une section dédiée à ce sujet.

Il est particulièrement important de gérer vos références lors de l'utilisation de fermetures. Par défaut, les fermetures (ou blocs) conservent une référence forte à chaque objet qui y est référencé. Si l'un de ces objets a une forte référence à la fermeture elle-même, nous avons un cycle de référence fort. Il est nécessaire d'utiliser des listes de capture pour bien gérer la capture de vos références.

S'il est possible que l'instance capturée par le bloc soit désallouée avant que le bloc ne soit appelé, vous devez la capturer en tant que référence faible , qui sera facultative car elle peut être nil . Maintenant, si vous êtes sûr que l'instance capturée ne sera pas désallouée pendant la durée de vie du bloc, vous pouvez la capturer en tant que référence sans propriétaire . L'avantage d'utiliser unowned au lieu de weak est que la référence ne sera pas facultative et que vous pouvez utiliser la valeur directement sans avoir besoin de la déballer.

Dans l'exemple suivant, que vous pouvez exécuter dans Xcode Playground, la classe Container a un tableau et une fermeture facultative qui est invoquée chaque fois que son tableau change (elle utilise des observateurs de propriété pour ce faire). La classe Whatever a une instance Container et, dans son initialiseur, elle affecte une fermeture à arrayDidChange et cette fermeture fait référence à self , créant ainsi une relation forte entre l'instance Whatever et la fermeture.

 struct Container<T> { var array: [T] = [] { didSet { arrayDidChange?(array: array) } } var arrayDidChange: ((array: [T]) -> Void)? } class Whatever { var container: Container<String> init() { container = Container<String>() container.arrayDidChange = { array in self.f(array) } } deinit { print("deinit whatever") } func f(s: [String]) { print(s) } } var w: Whatever! = Whatever() // ... w = nil

Si vous exécutez cet exemple, vous remarquerez que deinit whatever n'est jamais imprimé, ce qui signifie que notre instance w n'est pas désallouée de la mémoire. Pour résoudre ce problème, nous devons utiliser une liste self capture pour ne pas nous capturer fortement :

 struct Container<T> { var array: [T] = [] { didSet { arrayDidChange?(array: array) } } var arrayDidChange: ((array: [T]) -> Void)? } class Whatever { var container: Container<String> init() { container = Container<String>() container.arrayDidChange = { [unowned self] array in self.f(array) } } deinit { print("deinit whatever") } func f(s: [String]) { print(s) } } var w: Whatever! = Whatever() // ... w = nil

Dans ce cas, nous pouvons utiliser unowned , car self ne sera jamais nil pendant la durée de vie de la fermeture.

C'est une bonne pratique d'utiliser presque toujours des listes de capture pour éviter les cycles de référence, ce qui réduira les fuites de mémoire et un code plus sûr à la fin.

3. Utiliser self -même partout

Contrairement à Objective-C, avec Swift, nous ne sommes pas obligés d'utiliser self pour accéder aux propriétés d'une classe ou d'un struct à l'intérieur d'une méthode. Nous ne sommes tenus de le faire que dans une fermeture parce qu'elle a besoin de self capturer. Utiliser self là où ce n'est pas nécessaire n'est pas exactement une erreur, cela fonctionne très bien, et il n'y aura ni erreur ni avertissement. Cependant, pourquoi écrire plus de code que nécessaire ? De plus, il est important de garder votre code cohérent.

4. Ne pas connaître le type de vos types

Swift utilise des types valeur et des types référence . De plus, les instances d'un type valeur présentent un comportement légèrement différent des instances de types référence. Ne pas savoir à quelle catégorie appartient chacune de vos instances entraînera de fausses attentes sur le comportement du code.

Dans les langages les plus orientés objet, lorsque nous créons une instance d'une classe et que nous la passons à d'autres instances et comme argument de méthodes, nous nous attendons à ce que cette instance soit la même partout. Cela signifie que tout changement sera reflété partout, car en fait, ce que nous avons n'est qu'un tas de références aux mêmes données exactes. Les objets qui présentent ce comportement sont des types de référence, et dans Swift, tous les types déclarés comme class sont des types de référence.

Ensuite, nous avons des types de valeur qui sont déclarés à l'aide de struct ou enum . Les types valeur sont copiés lorsqu'ils sont affectés à une variable ou transmis en tant qu'argument à une fonction ou à une méthode. Si vous modifiez quelque chose dans l'instance copiée, l'instance d'origine ne sera pas modifiée. Les types de valeur sont immuables . Si vous affectez une nouvelle valeur à une propriété d'une instance d'un type de valeur, tel que CGPoint ou CGSize , une nouvelle instance est créée avec les modifications. C'est pourquoi nous pouvons utiliser des observateurs de propriété sur un tableau (comme dans l'exemple ci-dessus dans la classe Container ) pour nous informer des changements. Ce qui se passe réellement, c'est qu'un nouveau tableau est créé avec les modifications ; il est affecté à la propriété, puis didSet est appelé.

Ainsi, si vous ne savez pas que l'objet auquel vous avez affaire est de type référence ou valeur, vos attentes concernant ce que votre code va faire peuvent être totalement erronées.

5. Ne pas utiliser le plein potentiel des énumérations

Lorsque nous parlons d'énumérations, nous pensons généralement à l'énumération C de base, qui n'est qu'une liste de constantes liées qui sont des entiers en dessous. Dans Swift, les énumérations sont bien plus puissantes. Par exemple, vous pouvez associer une valeur à chaque cas d'énumération. Les énumérations ont également des méthodes et des propriétés en lecture seule/calculées qui peuvent être utilisées pour enrichir chaque cas avec plus d'informations et de détails.

La documentation officielle sur les énumérations est très intuitive et la documentation sur la gestion des erreurs présente quelques cas d'utilisation de la puissance supplémentaire des énumérations dans Swift. Consultez également la suite de l'exploration approfondie des énumérations dans Swift pour apprendre à peu près tout ce que vous pouvez faire avec elles.

6. Ne pas utiliser les fonctionnalités fonctionnelles

La Swift Standard Library fournit de nombreuses méthodes qui sont fondamentales dans la programmation fonctionnelle et nous permettent de faire beaucoup avec une seule ligne de code, comme mapper, réduire et filtrer, entre autres.

Examinons quelques exemples.

Supposons que vous deviez calculer la hauteur d'une vue de tableau. Étant donné que vous avez une sous-classe UITableViewCell telle que la suivante :

 class CustomCell: UITableViewCell { // Sets up the cell with the given model object (to be used in tableView:cellForRowAtIndexPath:) func configureWithModel(model: Model) // Returns the height of a cell for the given model object (to be used in tableView:heightForRowAtIndexPath:) class func heightForModel(model: Model) -> CGFloat }

Considérez, nous avons un tableau d'instances de modèle modelArray ; nous pouvons calculer la hauteur de la vue table avec une seule ligne de code :

 let tableHeight = modelArray.map { CustomCell.heightForModel($0) }.reduce(0, combine: +)

La map affichera un tableau de CGFloat , contenant la hauteur de chaque cellule, et la reduce les additionnera.

Si vous souhaitez supprimer des éléments d'un tableau, vous pouvez effectuer les opérations suivantes :

 var supercars = ["Lamborghini", "Bugatti", "AMG", "Alfa Romeo", "Koenigsegg", "Porsche", "Ferrari", "McLaren", "Abarth", "Morgan", "Caterham", "Rolls Royce", "Audi"] func isSupercar(s: String) -> Bool { return s.characters.count > 7 } for s in supercars { if !isSupercar(s), let i = supercars.indexOf(s) { supercars.removeAtIndex(i) } }

Cet exemple n'a pas l'air élégant, ni très efficace puisque nous appelons indexOf pour chaque élément. Considérez l'exemple suivant :

 var supercars = ["Lamborghini", "Bugatti", "AMG", "Alfa Romeo", "Koenigsegg", "Porsche", "Ferrari", "McLaren", "Abarth", "Morgan", "Caterham", "Rolls Royce", "Audi"] func isSupercar(s: String) -> Bool { return s.characters.count > 7 } for (i, s) in supercars.enumerate().reverse() { // reverse to remove from end to beginning if !isSupercar(s) { supercars.removeAtIndex(i) } }

Maintenant, le code est plus efficace, mais il peut encore être amélioré en utilisant le filter :

 var supercars = ["Lamborghini", "Bugatti", "AMG", "Alfa Romeo", "Koenigsegg", "Porsche", "Ferrari", "McLaren", "Abarth", "Morgan", "Caterham", "Rolls Royce", "Audi"] func isSupercar(s: String) -> Bool { return s.characters.count > 7 } supercars = supercars.filter(isSupercar)

L'exemple suivant illustre comment vous pouvez supprimer toutes les sous-vues d'un UIView qui répondent à certains critères, tels que le cadre coupant un rectangle particulier. Vous pouvez utiliser quelque chose comme :

 for v in view.subviews { if CGRectIntersectsRect(v.frame, rect) { v.removeFromSuperview() } } ``` We can do that in one line using `filter` ``` view.subviews.filter { CGRectIntersectsRect($0.frame, rect) }.forEach { $0.removeFromSuperview() }

Nous devons cependant être prudents, car vous pourriez être tenté d'enchaîner quelques appels à ces méthodes pour créer un filtrage et une transformation fantaisistes, qui peuvent aboutir à une ligne de code spaghetti illisible.

7. Rester dans la zone de confort et ne pas essayer la programmation orientée protocole

Swift est prétendu être le premier langage de programmation orienté protocole , comme mentionné dans la session WWDC Protocol-Oriented Programming in Swift. Fondamentalement, cela signifie que nous pouvons modéliser nos programmes autour de protocoles et ajouter un comportement aux types simplement en nous conformant aux protocoles et en les étendant. Par exemple, étant donné que nous avons un protocole Shape , nous pouvons étendre CollectionType (qui est conformé par des types tels que Array , Set , Dictionary ) et y ajouter une méthode qui calcule la surface totale en tenant compte des intersections

 protocol Shape { var area: Float { get } func intersect(shape: Shape) -> Shape? } extension CollectionType where Generator.Element: Shape { func totalArea() -> Float { let area = self.reduce(0) { (a: Float, e: Shape) -> Float in return a + e.area } return area - intersectionArea() } func intersectionArea() -> Float { /*___*/ } }

L'instruction where Generator.Element: Shape est une contrainte qui indique que les méthodes de l'extension ne seront disponibles que dans les instances de types conformes à CollectionType , qui contient des éléments de types conformes à Shape . Par exemple, ces méthodes peuvent être appelées sur une instance de Array<Shape> , mais pas sur une instance de Array<String> . Si nous avons une classe Polygon conforme au protocole Shape , ces méthodes seront également disponibles pour une instance de Array<Polygon> .

Avec les extensions de protocole, vous pouvez donner une implémentation par défaut aux méthodes déclarées dans le protocole, qui seront alors disponibles dans tous les types conformes à ce protocole sans avoir à apporter de modifications à ces types (classes, structs ou enums). Cela se fait largement dans la bibliothèque standard Swift, par exemple, la map et reduce sont définies dans une extension de CollectionType , et cette même implémentation est partagée par des types tels que Array et Dictionary sans aucun code supplémentaire.

Ce comportement est similaire aux mixins d'autres langages, tels que Ruby ou Python. En vous conformant simplement à un protocole avec des implémentations de méthode par défaut, vous ajoutez des fonctionnalités à votre type.

La programmation orientée protocole peut sembler assez maladroite et peu utile à première vue, ce qui pourrait vous faire l'ignorer et même ne pas lui donner une chance. Cet article donne une bonne compréhension de l'utilisation de la programmation orientée protocole dans des applications réelles.

Comme nous l'avons appris, Swift n'est pas un langage jouet

Swift a d'abord été accueilli avec beaucoup de scepticisme; les gens semblaient penser qu'Apple allait remplacer Objective-C par un langage jouet pour les enfants ou par quelque chose pour les non-programmeurs. Cependant, Swift s'est avéré être un langage sérieux et puissant qui rend la programmation très agréable. Comme il est fortement typé, il est difficile de faire des erreurs et, en tant que tel, il est difficile de répertorier les erreurs que vous pouvez commettre avec le langage.

Lorsque vous vous habituerez à Swift et que vous reviendrez à Objective-C, vous remarquerez la différence. Vous manquerez de fonctionnalités intéressantes offertes par Swift et devrez écrire du code fastidieux en Objective-C pour obtenir le même effet. D'autres fois, vous rencontrerez des erreurs d'exécution que Swift aurait détectées lors de la compilation. C'est une excellente mise à niveau pour les programmeurs Apple, et il reste encore beaucoup à faire à mesure que le langage mûrit.

Connexes : Guide du développeur iOS : d'Objective-C à Learning Swift