La puissance des génériques dans Swift. Partie 1

Bonjour à tous! Nous partageons avec vous une traduction préparée spécialement pour les étudiants du cours «Développeur iOS. Cours avancé . " Bonne lecture.



Fonction générique, type générique et restrictions de type

Que sont les génériques?


Quand ils travaillent, vous les aimez et quand non, vous les détestez!

Dans la vraie vie, tout le monde connaßt le pouvoir des génériques: se réveiller le matin, décider quoi boire, remplir une tasse.

Swift est un langage Ă  caractĂšres sĂ»rs. Chaque fois que nous travaillons avec des types, nous devons les spĂ©cifier explicitement. Par exemple, nous avons besoin d'une fonction qui fonctionnera avec plusieurs types. Swift a les types Any et AnyObject , mais ils doivent ĂȘtre utilisĂ©s avec AnyObject et pas toujours. L'utilisation de Any et AnyObject rendra votre code peu fiable, car il sera impossible de suivre la non-concordance de type pendant la compilation. C'est lĂ  que les gĂ©nĂ©riques viennent Ă  la rescousse.

Le code générique vous permet de créer des fonctions et des types de données réutilisables qui peuvent fonctionner avec tout type répondant à certaines restrictions, tout en garantissant la sécurité des types lors de la compilation. Cette approche vous permet d'écrire du code qui aide à éviter la duplication et exprime sa fonctionnalité de maniÚre claire et abstraite. Par exemple, des types tels que Array , Set et Dictionary utilisent des génériques pour stocker des éléments.

Disons que nous devons créer un tableau composé de valeurs entiÚres et de chaßnes. Pour résoudre ce problÚme, je vais créer deux fonctions.

 let intArray = [1, 2, 3, 4] let stringArray = [a, b, c, d] func printInts(array: [Int]) { print(intArray.map { $0 }) } func printStrings(array: [String]) { print(stringArray.map { $0 }) } 

Maintenant, je dois sortir un tableau d'éléments de type float ou un tableau d'objets utilisateur. Si nous regardons les fonctions ci-dessus, nous verrons que seule la différence de type est utilisée. Par conséquent, au lieu de dupliquer le code, nous pouvons écrire une fonction générique à réutiliser.

L'histoire des génériques dans Swift




Fonctions génériques


La fonction gĂ©nĂ©rique peut fonctionner avec n'importe quel paramĂštre universel de type T Le nom du type ne dit rien sur ce que devrait ĂȘtre , mais il indique que les deux tableaux doivent ĂȘtre de type , quel que soit Le type lui-mĂȘme Ă  utiliser au lieu de est dĂ©terminĂ© Ă  chaque appel de la fonction print( _: ) .

 func print<T>(array: [T]) { print(array.map { $0 }) } 

Types génériques ou polymorphisme paramétrique


Le type générique T de l'exemple ci-dessus est un paramÚtre de type. Vous pouvez spécifier plusieurs paramÚtres de type en écrivant plusieurs noms de paramÚtres de type entre crochets, séparés par des virgules.

Si vous regardez Array and Dictionary <Key, Element>, vous remarquerez qu'ils ont des paramÚtres de type nommés, c'est-à-dire Element et Key, Element, qui parlent de la relation entre le paramÚtre de type et le type ou la fonction générique dans lequel il est utilisé. .

Remarque: Donnez toujours des noms aux paramĂštres de type dans une notation CamelCase (par exemple, T et TypeParameter ) pour montrer qu'ils sont un nom pour le type, pas une valeur.

Types génériques


Ce sont des classes, des structures et des énumérations personnalisées qui peuvent fonctionner avec n'importe quel type, comme les tableaux et les dictionnaires.

Créons une pile

 import Foundation enum StackError: Error { case Empty(message: String) } public struct Stack { var array: [Int] = [] init(capacity: Int) { array.reserveCapacity(capacity) } public mutating func push(element: Int) { array.append(element) } public mutating func pop() -> Int? { return array.popLast() } public func peek() throws -> Int { guard !isEmpty(), let lastElement = array.last else { throw StackError.Empty(message: "Array is empty") } return lastElement } func isEmpty() -> Bool { return array.isEmpty } } extension Stack: CustomStringConvertible { public var description: String { let elements = array.map{ "\($0)" }.joined(separator: "\n") return elements } } var stack = Stack(capacity: 10) stack.push(element: 1) stack.push(element: 2) print(stack) stack.pop() stack.pop() stack.push(element: 5) stack.push(element: 3) stack.push(element: 4) print(stack) 

Maintenant, cette pile est capable d'accepter uniquement des éléments entiers, et si j'ai besoin de stocker des éléments d'un type différent, je devrai soit créer une autre pile, soit la convertir en une apparence générique.

 enum StackError: Error { case Empty(message: String) } public struct Stack<T> { var array: [T] = [] init(capacity: Int) { array.reserveCapacity(capacity) } public mutating func push(element: T) { array.append(element) } public mutating func pop() -> T? { return array.popLast() } public func peek() throws -> T { guard !isEmpty(), let lastElement = array.last else { throw StackError.Empty(message: "Array is empty") } return lastElement } func isEmpty() -> Bool { return array.isEmpty } } extension Stack: CustomStringConvertible { public var description: String { let elements = array.map{ "\($0)" }.joined(separator: "\n") return elements } } var stack = Stack<Int>(capacity: 10) stack.push(element: 1) stack.push(element: 2) print(stack) var strigStack = Stack<String>(capacity: 10) strigStack.push(element: "aaina") print(strigStack) 

Limitations des types génériques


Puisqu'un gĂ©nĂ©rique peut ĂȘtre de tout type, vous ne pouvez pas en faire grand-chose. Il est parfois utile d'appliquer des contraintes aux types pouvant ĂȘtre utilisĂ©s avec des fonctions gĂ©nĂ©riques ou des types gĂ©nĂ©riques. Les restrictions de type indiquent que le paramĂštre de type doit correspondre Ă  un protocole ou une composition de protocole spĂ©cifique.

Par exemple, le type Swift Dictionary impose des restrictions sur les types qui peuvent ĂȘtre utilisĂ©s comme clĂ©s pour un dictionnaire. Le dictionnaire nĂ©cessite que les clĂ©s soient hachĂ©es afin de pouvoir vĂ©rifier s'il contient dĂ©jĂ  des valeurs pour une clĂ© particuliĂšre.

 func someFunction<T: SomeClass, U: SomeProtocol>(someT: T, someU: U) { // function body goes here } 

Essentiellement, nous avons créé une pile de type T, mais nous ne pouvons pas comparer deux piles, car ici les types ne correspondent pas à Equatable . Nous devons changer cela pour utiliser Stack< T: Equatable > .

Comment fonctionnent les génériques? Regardons un exemple.

 func min<T: Comparable>(_ x: T, _ y: T) -> T { return y < x ? y : x } 

Le compilateur manque de deux choses nécessaires pour créer le code de fonction:

  • Tailles des variables de type T;
  • Les adresses de la surcharge spĂ©cifique de la fonction <, qui doit ĂȘtre appelĂ©e au moment de l'exĂ©cution.

Chaque fois que le compilateur rencontre une valeur de type gĂ©nĂ©rique, il place la valeur dans un conteneur. Ce conteneur a une taille fixe pour stocker des valeurs. Dans le cas oĂč la valeur est trop grande, Swift l'alloue sur le tas et stocke un lien vers celle-ci dans le conteneur.

Le compilateur conserve également une liste d'une ou plusieurs tables témoins pour chaque paramÚtre générique: une table témoin pour les valeurs, plus une table témoin pour chaque type de protocole de restriction. Les tables témoins sont utilisées pour envoyer dynamiquement des appels de fonction aux implémentations souhaitées lors de l'exécution.

La fin de la premiĂšre partie. Par tradition, nous attendons vos commentaires, amis.

DeuxiĂšme partie

Source: https://habr.com/ru/post/fr462753/


All Articles