Programmation orientée protocole, partie 3

Article final sur la programmation orientée protocole.


Dans cette partie, nous verrons comment les variables de type générique sont stockées et copiées et comment la méthode de répartition fonctionne avec elles.


Version non partagée


protocol Drawable { func draw() } func drawACopy(local: Drawable) { local.draw() } let line = Line() drawACopy(line) let point = Point() drawACopy(point) 

Code très simple. drawACopy prend un paramètre de type Drawable et appelle sa méthode draw - c'est tout.


Version généralisée


Regardons la version généralisée du code ci-dessus:


 func drawACopy<T: Drawable>(local: T) { local.draw() } ... 

Rien ne semble avoir changé. On peut toujours appeler la fonction drawACopy , comme sa version drawACopy , et rien de plus, mais la plus intéressante comme d'habitude sous le capot.
Le code généralisé a deux caractéristiques importantes:


  1. polymorphisme statique (également appelé paramétrique)
  2. un type spécifique et unique dans le contexte de l'appel (un type générique T est défini au moment de la compilation)

Considérez ceci avec un exemple:


 func foo<T: Drawable>(local: T) { bar(local) } func bar<T: Drawable>(local: T) { ... } let point = Point(...) foo(point) 

La partie la plus intéressante commence lorsque nous appelons la fonction foo . Le compilateur connaît exactement le type du point variable - c'est juste Point. De plus, le type T: Drawable dans la fonction foo peut être librement déduit par le compilateur à partir du moment où nous transmettons une variable du type Point connu à cette fonction: T = Point. Tous les types sont connus au moment de la compilation et le compilateur peut effectuer toutes ses optimisations merveilleuses - la chose la plus importante est d'inline l'appel foo .


 This: ```swift let point = Point(...) foo<T = Point>(point) Becomes this: ```swift bar<T = Point>(point) 

Le compilateur intègre simplement l'appel foo avec son implémentation et affiche également le type générique de barre T: Drawable. En d'autres termes, le compilateur incorpore d'abord un appel à la méthode foo avec le type T = Point, puis il incorpore le résultat de l'incorporation précédente - la méthode bar avec le type T = Point.


Implémentation de méthodes génériques


 func drawACopy<T: Drawable>(local: T) { local.draw() } drawACopy(Point(...)) 

En interne, drawACopy Swift utilise une table de méthodes de protocole (qui contient toutes les implémentations de la méthode T) et une table de cycle de vie (qui contient toutes les méthodes de cycle de vie pour l'instance T). En pseudocode, cela ressemble à ceci:


 func drawACopy<T: Drawable>(local: T, pwt: T.PWT, vwt: T.VWT) {...} drawACopy(Point(...), Point.pwt, Point.vwt) 

VWT et PWT sont des types associés (type associé) dans T - en tant qu'alias de type (typealias), mais en mieux. Point.pwt et Point.vwt sont des propriétés statiques.


Puisque dans notre exemple T est Point, T est bien défini, par conséquent, la création d'un conteneur n'est pas requise. Dans la précédente version drawACopy de drawACopy (local: Drawable), la création d'un conteneur existentiel était effectuée si nécessaire - nous l'avons examiné dans la deuxième partie de l'article.


Une table de cycle de vie est requise dans les fonctions en raison de la création d'un argument. Comme nous le savons, les arguments dans Swift passent par des valeurs, pas par des liens, par conséquent, ils doivent être copiés, et la méthode de copie de cet argument appartient à la table du cycle de vie comme cet argument. Il existe également d'autres méthodes de cycle de vie: allouer, détruire et désallouer.


Une table de cycle de vie est requise dans les fonctions génériques en raison de l'utilisation de méthodes pour les paramètres de code génériques.


Généralisé ou non généralisé?


Est-il vrai que l'utilisation de types génériques rend l'exécution de code plus rapide que l'utilisation de types de protocoles uniquement? La fonction générique func foo<T: Drawable>(arg: T) plus rapide que son homologue de type protocole fun foo(arg: Drawable) ?


Nous avons remarqué que le code générique donne une forme plus statique de polymorphisme. Il inclut également des optimisations de compilateur appelées «spécialisation de code générique». Voyons voir:


Encore une fois, nous avons le même code:


 func drawACopy<T: Drawable>(local: T) { local.draw() } drawACopy(Point(...)) drawACopt(Line(...)) 

La spécialisation d'une fonction générique crée une copie avec des types génériques spécialisés de cette fonction. Par exemple, si nous appelons drawACopy avec une variable de type Point, le compilateur créera une version spécialisée de cette fonction - drawACopyOfPoint (local: Point), et nous obtenons:


 func drawACopyOfPoint(local: Point) { local.draw() } func drawACopyOfLine(local: Line) { local.draw() } drawACopy(Point(...)) drawACopt(Line(...)) 

Ce qui peut être réduit par l'optimisation du compilateur brut avant cela:


 Point(...).draw() Line(...).draw() 

Toutes ces astuces sont disponibles car les fonctions génériques ne peuvent être appelées que si tous les types génériques sont définis - dans la méthode drawACopy type générique (T) est bien défini.


Propriétés stockées génériques


Considérons une simple paire de structures:


 struct Pair { let fst: Drawable let snd: Drawable } let pair = Pair(fst: Line(...), snd: Line(...)) 

Lorsque nous utilisons cela de cette manière, nous obtenons 2 allocations sur le tas (les conditions de mémoire exactes dans ce scénario ont été décrites dans la deuxième partie), mais nous pouvons éviter cela à l'aide d'un code généralisé.


La version générique de Pair ressemble à ceci:


 struct Pair<T: Drawable> { let fst: T let snd: T } 

A partir du moment où le type T est défini dans la version généralisée, les types de propriété fst et snd mêmes et sont également définis. Puisque le type est défini, le compilateur peut allouer une quantité spécialisée de mémoire pour ces deux propriétés - fst et snd .


Plus en détail sur la quantité de mémoire spécialisée:


lorsque nous travaillons avec une version fst de Pair , les types de propriété fst et snd sont Drawable. Tout type peut correspondre à Drawable, même s'il prend 10 Ko de mémoire. Autrement dit, Swift ne sera pas en mesure de tirer une conclusion sur la taille de ce type et utilisera un emplacement de mémoire universel, par exemple, un conteneur existentiel. Tout type peut être stocké dans ce conteneur. Dans le cas du code générique, le type est bien reconnu, la taille réelle des propriétés est également reconnaissable et Swift peut créer un emplacement de mémoire spécialisé. Par exemple (version généralisée):


 let pair = Pair(Point(...), Point(...)) 

Le type T est maintenant Point. Le point prend N octets de mémoire et dans Pair, nous en obtenons deux. Swift allouera 2 * N de mémoire et y placera une pair .


Ainsi, avec la version généralisée de Pair, nous nous débarrassons des allocations inutiles sur le tas, car les types sont facilement reconnaissables et peuvent être localisés spécifiquement - sans avoir besoin de créer des modèles de mémoire universelle, car tout est connu.


Conclusion


1. Code générique spécialisé - Types de valeurs


a la meilleure vitesse d'exécution, car:


  • aucune allocation de tas lors de la copie
  • code générique - vous écrivez une fonction pour un type spécialisé
  • pas de référence
  • répartition de méthode statique

2. Code généralisé spécialisé - types de référence


Il a une vitesse d'exécution moyenne, car:


  • allocations par segment lors de l'instanciation
  • il y a un compte de référence
  • soumission de méthode dynamique via une table virtuelle

3. Code généralisé non spécialisé - petites valeurs


  • aucune allocation de tas - la valeur est placée dans le tampon de valeur du conteneur existentiel
  • pas de comptage de référence (puisque rien n'est placé sur le tas)
  • envoi de méthode dynamique via une table de méthode de protocole

4. Code généralisé non spécialisé - grandes valeurs


  • placement sur le tas - la valeur est placée dans le tampon de valeurs
  • il y a un compte de référence
  • répartition dynamique via une table de méthode de protocole

Ce matériel ne signifie pas que les classes sont mauvaises, les structures sont bonnes et les structures en combinaison avec du code généralisé sont les meilleures. Nous voulons dire qu'en tant que programmeur, vous avez la responsabilité de choisir un outil pour vos tâches. Les classes sont vraiment bonnes lorsque vous devez conserver de grandes valeurs et qu'il existe une sémantique de liens. Les structures sont idéales pour les petites valeurs et lorsque vous avez besoin de leur sémantique. Les protocoles sont mieux adaptés au code et aux structures génériques, etc. Tous les outils sont spécifiques à la tâche que vous résolvez et ont des côtés positifs et négatifs.


Et aussi ne payez pas pour le dynamisme quand vous n'en avez pas besoin . Trouvez la bonne abstraction avec le moins d'exigences d'exécution.


  • types de structure - sémantique des significations
  • types de classe - identité
  • code généralisé - polymorphisme statique
  • types de protocoles - polymorphisme dynamique

Utilisez le stockage indirect pour travailler avec de grandes valeurs.


Et n'oubliez pas - il vous appartient de choisir le bon outil.
Merci de votre attention sur ce sujet. Nous espérons que ces articles vous ont aidé et étaient intéressants.


Bonne chance

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


All Articles