Le code générique vous permet d'écrire des fonctions et des types flexibles et réutilisables qui peuvent fonctionner avec n'importe quel type, sous réserve des exigences que vous définissez. Vous pouvez écrire du code qui évite la duplication et exprime son intention de manière claire et abstraite. - Documents rapides
Tous ceux qui ont écrit sur Swift ont utilisé des génériques. Array
, Dictionary
, Set
- les options les plus élémentaires pour l'utilisation des génériques de la bibliothèque standard. Comment sont-ils représentés à l'intérieur? Voyons comment cette fonctionnalité fondamentale du langage est mise en œuvre par les ingénieurs d'Apple.
Les paramètres génériques peuvent être limités par des protocoles ou non limités, bien que, fondamentalement, les génériques soient utilisés conjointement avec des protocoles qui décrivent exactement ce qui peut être fait avec les paramètres de méthode ou les champs de type.
Pour implémenter des génériques, Swift utilise deux approches:
- Runtime-way - le code générique est un wrapper (Boxing).
- Compiletime-way - le code générique est converti en un type de code spécifique pour l'optimisation (spécialisation).
La boxe
Considérons une méthode simple avec un paramètre générique de protocole illimité:
func test<T>(value: T) -> T { let copy = value print(copy) return copy }
Le compilateur rapide crée un seul bloc de code qui sera appelé pour fonctionner avec n'importe quel <T>
. Autrement dit, que nous écrivions test(value: 1)
ou test(value: "Hello")
, le même code sera appelé et des informations supplémentaires sur le type <T>
contenant toutes les informations nécessaires seront transférées vers la méthode .
Peu de choses peuvent être faites avec de tels paramètres de protocole illimités, mais déjà pour implémenter cette méthode, vous devez savoir comment copier un paramètre, vous devez connaître sa taille afin de lui allouer de la mémoire lors de l'exécution, vous devez savoir comment le détruire lorsque le paramètre quitte le champ visibilité. La Value Witness Table
( VWT
) est utilisée pour stocker ces informations. VWT
est créé au stade de la compilation pour tous les types et le compilateur garantit qu'en exécution, il y aura exactement une telle disposition d'objet. Permettez-moi de vous rappeler que les structures dans Swift sont passées par valeur et les classes par référence, donc différentes choses seront faites pour let copy = value
avec T == MyClass
et T == MyStruct
.
C'est-à -dire qu'en appelant la méthode de test
en passant la structure déclarée, cela ressemblera finalement à ceci:
Les choses deviennent un peu plus compliquées lorsque MyStruct
lui MyStruct
même une structure générique et prend la forme MyStruct<T>
. Selon le <T>
intérieur de MyStruct
, les métadonnées et VWT
seront différents pour les types MyStruct<Int>
et MyStruct<Bool>
. Ce sont deux types différents lors de l'exécution. Mais la création de métadonnées pour chaque combinaison possible de MyStruct
et T
extrêmement inefficace, donc Swift va dans l'autre sens et, dans de tels cas, construit des métadonnées lors de l'exécution en déplacement. Le compilateur crée un modèle de métadonnées pour la structure générique, qui peut être combiné avec un type spécifique et, par conséquent, recevoir des informations de type complètes au moment de l'exécution avec le VWT
correct.
Lorsque nous combinons des informations, nous obtenons des métadonnées avec lesquelles nous pouvons travailler (copier, déplacer, détruire).
C'est encore un peu plus compliqué lorsque des restrictions de protocole sont ajoutées aux génériques. Par exemple, nous limitons <T>
protocole Equatable
. Que ce soit une méthode très simple qui compare les deux arguments passés. Le résultat est juste un wrapper sur la méthode de comparaison.
func isEquals<T: Equatable>(first: T, second: T) -> Bool { return first == second }
Pour que le programme fonctionne correctement, vous devez avoir un pointeur sur la méthode de comparaison static func ==(lhs:T, rhs:T)
. Comment l'obtenir? Évidemment, la transmission VWT
ne suffit pas, elle ne contient pas cette information. Pour résoudre ce problème, il existe une Protocol Witness Table
ou PWT
. Cette VWT
est similaire Ă VWT
et est créée au stade de la compilation des protocoles et décrit ces protocoles.
isEquals(first: 1, second: 2)
- Deux arguments passés
- Passez des métadonnées pour
Int
afin de pouvoir copier / déplacer / détruire des objets - Nous transmettons les informations
Equatable
implémente Equatable
.
Si la restriction nécessitait l'implémentation d'un autre protocole, par exemple, T: Equatable & MyProtocol
, des informations sur MyProtocol
seraient ajoutées avec le paramètre suivant:
isEquals(..., intIsEquatable: Equatable.witnessTable, intIsMyProtocol: MyProtocol.witnessTable)
L'utilisation de wrappers pour implémenter des génériques vous permet d'implémenter de manière flexible toutes les fonctionnalités nécessaires, mais a une surcharge qui peut être optimisée.
Spécialisation générique
Pour éliminer le besoin inutile d'obtenir des informations pendant l'exécution du programme, l'approche dite de spécialisation générique a été utilisée. Il vous permet de remplacer un wrapper générique par un type spécifique par une implémentation spécifique. Par exemple, pour deux appels à isEquals(first: 1, second: 2)
et isEquals(first: "Hello", second: "world")
, en plus de l'implémentation principale "wrapper", deux versions supplémentaires complètement différentes de la méthode pour Int
et pour String
.
Code source
Tout d'abord, créez un fichier generic.swift et écrivez une petite fonction générique que nous considérerons.
func isEquals<T: Equatable>(first: T, second: T) -> Bool { return first == second } isEquals(first: 10, second: 11)
Vous devez maintenant comprendre ce qu'il se transforme finalement en compilateur.
Cela peut ĂŞtre clairement vu en compilant notre fichier .swift en Swift Intermediate Language ou SIL
.
Un peu sur SIL et le processus de compilation
SIL
est le résultat d'une des nombreuses étapes de la compilation rapide.
Le code source .swift est transmis à Lexer, qui crée un arbre de syntaxe abstraite ( AST
) du langage, basé sur la vérification de type et l'analyse sémantique du code. SilGen convertit AST
en SIL
, appelé raw SIL
, sur la base duquel le code est optimisé et un canonical SIL
optimisé canonical SIL
obtenu, qui est transmis Ă IRGen
pour la conversion en IR
- un format spécial que LLVM
comprend, qui sera converti en , .
.o , .
, .
SIL`.
Et encore aux génériques
Créez un fichier SIL
Ă partir de notre code source.
swiftc generic.swift -O -emit-sil -o generic-sil.s
Nous obtenons un nouveau fichier avec l'extension *.s
. En regardant à l'intérieur, nous verrons un code beaucoup moins lisible que l'original, mais toujours relativement clair.
Trouvez la ligne avec le commentaire // isEquals<A>(first:second:)
. C'est le début de la description de notre méthode. Il se termine par un commentaire // end sil function '$s4main8isEquals5first6secondSbx_xtSQRzlF'
. Votre nom peut être légèrement différent. Analysons un peu la description de la méthode.
%0
et %1
sur la ligne 21 sont les first
et second
paramètres, respectivement- Sur la ligne 24, nous obtenons les informations de type et les transmettons Ă
%4
- Sur la ligne 25, nous obtenons un pointeur vers une méthode de comparaison à partir des informations de type
- on line 26 On appelle la méthode par pointeur, en lui passant à la fois des paramètres et des informations de type
- À la ligne 27, nous donnons le résultat.
En conséquence, nous voyons: afin d'effectuer les actions nécessaires dans la mise en œuvre de la méthode générique, nous devons obtenir des informations à partir de la description du type <T>
lors de l'exécution du programme.
Nous procédons directement à la spécialisation.
Dans le fichier SIL
compilé, immédiatement après la déclaration de la méthode générale isEquals
, la déclaration du spécialisé pour le type Int
suit.
Sur la ligne 39, au lieu d'obtenir la méthode lors de l'exécution à partir des informations de type, la méthode de comparaison des entiers "cmp_eq_Int64"
immédiatement appelée.
Pour que la méthode se «spécialise», l' optimisation doit être activée . Vous devez également savoir que
L'optimiseur ne peut effectuer une spécialisation que si la définition de la déclaration générique est visible dans le module actuel ( source )
Autrement dit, la méthode ne peut pas être spécialisée entre différents modules Swift (par exemple, la méthode générique de la bibliothèque Cocoapods). Une exception est la bibliothèque Swift standard, dans laquelle des types de base tels que Array
, Set
et Dictionary
. Tous les génériques de la bibliothèque de base sont spécialisés dans des types spécifiques.
Remarque: Les attributs @inlinable
et @usableFromInline
ont été implémentés dans Swift 4.2, qui permettent à l'optimiseur de voir le corps des méthodes des autres modules et il semble qu'il y ait une opportunité de les spécialiser, mais ce comportement n'a pas été testé par moi ( Source )
Les références
- Description des génériques
- Optimisation dans Swift
- Présentation plus détaillée et approfondie du sujet.
- Article en anglais