Comment utiliser les génériques dans Swift

Les génériques vous permettent de déclarer une variable qui, à l'exécution, peut être affectée à un ensemble de types définis par nous..

Dans Swift, un tableau peut contenir des données de tout type. Si nous avons besoin d'un tableau d'entiers, de chaînes ou de flottants, nous pouvons en créer un avec la bibliothèque standard Swift. Le type que doit contenir le tableau est défini lors de sa déclaration. Les tableaux sont un exemple courant d'utilisation de génériques. Si vous deviez mettre en place votre propre collection, vous voudriez certainement utiliser des génériques. 

Explorons les génériques et quelles grandes choses ils nous permettent de faire.

1. Fonctions génériques

Nous commençons par créer une fonction générique simple. Notre objectif est de créer une fonction permettant de vérifier si deux objets sont du même type. S'ils sont du même type, alors nous allons rendre la valeur du deuxième objet égale à la valeur du premier objet. S'ils ne sont pas du même type, alors nous afficherons "pas le même type". Voici une tentative de mise en œuvre d'une telle fonction dans Swift.

func sameType (one: Int, inout two: Int) -> Void // Ce sera toujours le cas si (one.dynamicType == two.dynamicType) deux = un else print ("pas de même type") 

Dans un monde sans génériques, nous rencontrons un problème majeur. Dans la définition d'une fonction, nous devons spécifier le type de chaque argument. Par conséquent, si nous voulons que notre fonction fonctionne avec tous les types possibles, nous devrions écrire une définition de notre fonction avec des paramètres différents pour chaque combinaison de types possible. Ce n'est pas une option viable.

func sameType (one: Int, inout two: String) -> Void // Ceci serait toujours faux si (one.dynamicType == two.dynamicType) deux = un else print ("pas de même type") 

Nous pouvons éviter ce problème en utilisant des génériques. Jetez un oeil à l'exemple suivant dans lequel nous tirons parti des génériques.

func sameType(un: T, deux autres: E) -> Nul if (one.dynamicType == two.dynamicType) deux = un else print ("pas du même type")

Nous voyons ici la syntaxe d'utilisation des génériques. Les types génériques sont symbolisés par T et E. Les types sont spécifiés en mettant dans la définition de notre fonction, après le nom de la fonction. Penser à T et E en tant qu'espaces réservés pour le type, nous utilisons notre fonction avec.

Il y a cependant un problème majeur avec cette fonction. Ça ne compilera pas. Le compilateur avec une erreur, indiquant que T n'est pas convertible en E. Les génériques supposent que depuis T et E avoir des étiquettes différentes, ils seront également différents types. C'est bien, nous pouvons toujours atteindre notre objectif avec deux définitions de notre fonction..

func sameType(un: T, deux autres: E) -> Nul print ("pas de même type") func sameType(un: T, deux sur deux: T) -> Vide deux = un

Il y a deux cas pour les arguments de notre fonction:

  • S'ils sont du même type, la deuxième implémentation est appelée. La valeur de deux est ensuite affecté à un.
  • S'ils sont de types différents, la première implémentation est appelée et la chaîne "pas de même type" est imprimée sur la console.. 

Nous avons réduit nos définitions de fonctions d'un nombre potentiellement infini de combinaisons de types d'arguments à deux. Notre fonction fonctionne maintenant avec n'importe quelle combinaison de types comme arguments.

var s = "pomme" var p = 1 sameType (2, deux: & p) print (p) sameType ("pomme", deux: & p) // Sortie: 1 "pas du même type"

La programmation générique peut également être appliquée aux classes et aux structures. Voyons comment cela fonctionne.

2. Classes et structures génériques

Prenons le cas où nous aimerions créer notre propre type de données, un arbre binaire. Si nous utilisons une approche traditionnelle dans laquelle nous n'utilisons pas de génériques, nous créerions un arbre binaire ne pouvant contenir qu'un seul type de données. Heureusement, nous avons des génériques.

Un arbre binaire est constitué de nœuds qui ont:

  • deux enfants ou branches, qui sont d'autres noeuds
  • une donnée qui est l'élément générique
  • un nœud parent qui n'est généralement pas référencé par le nœud

Chaque arbre binaire a un noeud principal qui n’a pas de parents. Les deux enfants sont généralement différenciés en nœuds gauche et droit.

Toutes les données d'un enfant de gauche doivent être inférieures au nœud parent. Toutes les données du bon enfant doivent être supérieures au noeud parent..

classe BTree  var data: T? = nil var à gauche: BTree? = nil var right: BTree? = nil func insert (newData: T) if (self.data> newData) // Insérer dans le sous-arbre de gauche else if (self.data < newData)  // Insert into right subtree  else if (self.data == nil)  self.data = newData return   

La déclaration du BTree la classe déclare également le générique T, qui est contraint par le Comparable protocole. Nous allons discuter des protocoles et des contraintes dans un peu.

L'élément de données de notre arbre est spécifié pour être de type T. Tout élément inséré doit également être de type T tel que spécifié par la déclaration du insérer(_:) méthode. Pour une classe générique, le type est spécifié lorsque l'objet est déclaré.

var arbre: BTree

Dans cet exemple, nous créons un arbre binaire d’entiers. Faire un cours générique est assez simple. Tout ce que nous avons à faire est d'inclure le générique dans la déclaration et de le référencer dans le corps si nécessaire.

3. Protocoles et contraintes

Dans de nombreuses situations, nous devons manipuler des tableaux pour atteindre un objectif programmatique. Cela pourrait être le tri, la recherche, etc. Nous allons voir comment les génériques peuvent nous aider dans la recherche.

La raison principale pour laquelle nous utilisons une fonction générique pour la recherche est que nous voulons pouvoir rechercher un tableau, quel que soit le type d'objets qu'il contient..

func trouver  (tableau: [T], élément: T) -> Int? var index = 0 while (index < array.count)  if(item == array[index])  return index  index++  return nil; 

Dans l'exemple ci-dessus, le find (array: item :) fonction accepte un tableau du type générique T et cherche une correspondance pour article qui est aussi de type T.

Il y a un problème cependant. Si vous essayez de compiler l'exemple ci-dessus, le compilateur générera une autre erreur. Le compilateur nous dit que l'opérateur binaire == ne peut pas être appliqué à deux T des opérandes. La raison en est évidente si vous y réfléchissez. Nous ne pouvons pas garantir que le type générique T soutient le == opérateur. Heureusement, Swift a ce couvert. Jetez un coup d'œil à l'exemple mis à jour ci-dessous.

func trouver  (tableau: [T], élément: T) -> Int? var index = 0 while (index < array.count)  if(item == array[index])  return index  index++  return nil; 

Si nous spécifions que le type générique doit être conforme à la Équitable protocole, puis le compilateur nous donne une passe. En d'autres termes, nous appliquons une contrainte sur quels types T peut représenter. Pour ajouter une contrainte à un générique, vous listez les protocoles entre les chevrons..

Mais qu'est-ce que cela signifie pour que quelque chose soit Équitable? Cela signifie simplement qu'il supporte l'opérateur de comparaison ==.

Équitable n'est pas le seul protocole que nous pouvons utiliser. Swift a d’autres protocoles, tels que Hashableet Comparable. Nous avons vu Comparable plus tôt dans l'exemple de l'arbre binaire. Si un type est conforme à la Comparable protocole, cela signifie que le < et > les opérateurs sont pris en charge. J'espère qu'il est clair que vous pouvez utiliser n'importe quel protocole et l'appliquer comme contrainte.

4. Définir les protocoles

Utilisons un exemple de jeu pour démontrer les contraintes et les protocoles en action. Dans n'importe quel jeu, nous aurons un certain nombre d'objets à mettre à jour au fil du temps. Cette mise à jour pourrait porter sur la position, la santé, etc. de l'objet. Pour le moment, prenons l'exemple de la santé de l'objet..

Dans notre implémentation du jeu, nous avons beaucoup d'objets différents avec une santé qui pourraient être des ennemis, des alliés, des neutres, etc. Ils ne seraient pas tous de la même classe car tous nos objets différents pourraient avoir des fonctions différentes..

Nous aimerions créer une fonction appelée vérifier(_:)pour vérifier la santé d'un objet donné et mettre à jour son statut actuel. Selon le statut de l'objet, nous pouvons modifier sa santé. Nous voulons que cette fonction fonctionne sur tous les objets, quel que soit leur type. Cela signifie que nous devons faire vérifier(_:)une fonction générique. Ce faisant, nous pouvons parcourir les différents objets et appeler vérifier(_:) sur chaque objet.

Tous ces objets doivent avoir une variable pour représenter leur santé et une fonction pour changer leur vivant statut. Déclarons un protocole pour cela et nommons-le. En bonne santé.

protocole Healthy mutating func setAlive (statut: Bool) var santé: Int get

Le protocole définit les propriétés et les méthodes que le type conforme au protocole doit implémenter. Par exemple, le protocole exige que tout type conforme à la En bonne santé protocole implémente la mutation setAlive (_ :) une fonction. Le protocole nécessite également une propriété nommée santé.

Revisitons maintenant le vérifier(_:) fonction nous avons déclaré plus tôt. Nous spécifions dans la déclaration avec une contrainte que le type T doit se conformer à la En bonne santé protocole.

func check(inout object: T) if (object.health <= 0)  object.setAlive(false)  

Nous vérifions l'objet santé propriété. S'il est inférieur ou égal à zéro, on appelle setAlive (_ :) sur l'objet, en passant faux. Parce que T est tenu de se conformer à la En bonne santé protocole, nous savons que le setAlive (_ :) fonction peut être appelée sur n’importe quel objet qui est passé au vérifier(_:) une fonction.

5. Types associés

Si vous souhaitez mieux contrôler vos protocoles, vous pouvez utiliser les types associés. Revenons à l'exemple de l'arbre binaire. Nous aimerions créer une fonction pour effectuer des opérations sur un arbre binaire. Nous avons besoin d'un moyen de nous assurer que l'argument d'entrée satisfait à ce que nous définissons comme un arbre binaire. Pour résoudre ce problème, nous pouvons créer un BinaryTree protocole.

protocole BinaryTree typealias dataType mutant func insert (data: type de données) func index (i: int) -> type de données data type: type de données get 

Cela utilise un type associé typealias dataType. Type de données est similaire à un générique. T de plus tôt, se comporte de la même manière que Type de données. Nous spécifions qu'un arbre binaire doit implémenter les fonctions insérer(_:) et indice(_:)insérer(_:) accepte un argument de type Type de données. indice(_:) renvoie un Type de données objet. Nous spécifions également que l'arbre binaire doit havoir une propriété Les données c'est de type Type de données.

Grâce à notre type associé, nous savons que notre arbre binaire sera cohérent. On peut supposer que le type passé à insérer(_:), donné par indice(_:), et tenu par Les données est le même pour chacun. Si les types n'étaient pas tous les mêmes, nous aurions des problèmes.

6. Où Clause

Swift vous permet également d’utiliser des clauses where avec des génériques. Voyons comment cela fonctionne. Les articles nous permettent de réaliser des choses avec deux génériques:

  • Nous pouvons imposer que les types ou variables associés dans un protocole sont du même type.
  • On peut assigner un protocole à un type associé.

Pour montrer cela en action, implémentons une fonction permettant de manipuler des arbres binaires. Le but est de trouver la valeur maximale entre deux arbres binaires.

Par souci de simplicité, nous allons ajouter une fonction au BinaryTree protocole appelé en ordre(). En ordre est l'un des trois types de traversée les plus populaires en profondeur. C'est un ordre des noeuds de l'arbre qui se déplace de manière récursive, sous-arbre de gauche, noeud actuel, sous-arbre de droite.

protocole BinaryTree typealias dataType mutating func insert (data: dataType) func index (i: Int) -> dataType var data: dataType get // NEW func inorder () -> [dataType]

Nous attendons le en ordre() fonction pour retourner un tableau d'objets du type associé. Nous implémentons également la fonction deuxMax (treeOne: treeTwo :)qui accepte deux arbres binaires.

func deux max (inout treeOne: B, inout treeTwo: T) -> B.dataType var inorderOne = treeOne.inorder () var inorderTwo = treeTwo.inorder () if (inorderOne [inorderOne.count]> inorderTwo [inorderTwo.count])  return inorderOne [inorderOne.count] else return inorderTwo [inorderTwo.count]

Notre déclaration est assez longue en raison de la clause. La première exigence, B.dataType == T.dataType, stipule que les types associés des deux arbres binaires doivent être les mêmes. Cela signifie que leur Les données les objets doivent être du même type.

Le deuxième ensemble d'exigences, B.dataType: Comparable, T.dataType: Comparable, stipule que les types associés des deux doivent être conformes à la Comparable protocole. De cette façon, nous pouvons vérifier quelle est la valeur maximale lors d'une comparaison.

Fait intéressant, en raison de la nature d’un arbre binaire, nous savons que le dernier élément d’un élément en ordre sera l'élément maximum dans cet arbre. En effet, dans un arbre binaire, le nœud le plus à droite est le plus grand. Il suffit de regarder ces deux éléments pour déterminer la valeur maximale.

Nous avons trois cas:

  1. Si l'arbre 1 contient la valeur maximale, le dernier élément de l'inordeur sera le plus grand et nous le renverrons dans le premier si déclaration.
  2. Si l’arbre deux contient la valeur maximale, le dernier élément de son inorder sera le plus grand et nous le retournerons dans le autre clause de la première si déclaration.
  3. Si leurs maximums sont égaux, alors nous retournons le dernier élément de l'arbre deux en ordre, qui est toujours le maximum pour les deux.

Conclusion

Dans ce tutoriel, nous nous sommes concentrés sur les génériques dans Swift. Nous avons appris la valeur des génériques et exploré comment utiliser des génériques dans des fonctions, des classes et des structures. Nous avons également utilisé des génériques dans les protocoles et exploré les types associés et les clauses où.

Avec une bonne compréhension des génériques, vous pouvez maintenant créer un code plus polyvalent et vous pourrez mieux gérer les problèmes de codage difficiles..