Développement mobile. Swift: le mystère des protocoles

Aujourd'hui, nous continuons la série de publications sur le thème du développement mobile pour iOS. Et si la dernière fois que nous avons parlé de ce dont vous avez besoin et que vous n'avez pas besoin de demander lors des entretiens, dans ce document, nous aborderons le sujet des protocoles, qui est important dans Swift. Il s'agira de la façon dont les protocoles sont organisés, comment ils diffèrent les uns des autres et comment ils se combinent avec les interfaces Objective-C.



Comme nous l'avons dit précédemment, le nouveau langage Apple continue d'évoluer, et la plupart de ses paramètres et fonctionnalités sont clairement indiqués dans la documentation. Mais qui lit la documentation lorsque le code doit être écrit ici et maintenant? Passons donc en revue les principales caractéristiques des protocoles Swift dans notre article.

Pour commencer, il convient de noter que les protocoles Apple sont un terme alternatif pour le concept d '«Interface», qui est utilisé dans d'autres langages de programmation. Dans Swift, les protocoles sont utilisés pour indiquer les modèles de certaines structures (appelés plans directeurs) qui peuvent être travaillés à un niveau abstrait. En termes simples, le protocole définit un certain nombre de méthodes et de variables qu'un certain type doit hériter sans faute.

Plus loin dans l'article, les moments seront progressivement révélés comme suit: des plus simples et souvent utilisés aux plus complexes. En principe, lors des entretiens, vous pouvez poser des questions dans cet ordre, car elles déterminent le niveau de compétence du demandeur - du niveau des juniors au niveau des seniors.

Quels sont les protocoles nécessaires dans Swift?


Les développeurs mobiles se passent souvent de protocoles, mais ils perdent la possibilité de travailler de manière abstraite avec certaines entités. Si nous mettons en évidence les principales caractéristiques des protocoles dans Swift, nous obtenons les 7 points suivants:

  • Les protocoles fournissent un héritage multiple
  • Les protocoles ne peuvent pas stocker l'état
  • Les protocoles peuvent être hérités par d'autres protocoles.
  • Les protocoles peuvent être appliqués aux structures (struct), classes (classe) et énumérations (enum), définissant la fonctionnalité de type
  • Les protocoles génériques vous permettent de spécifier des dépendances complexes entre les types et les protocoles lors de leur héritage
  • Les protocoles ne définissent pas de références de variables «fortes» ou «faibles»
  • Dans les extensions des protocoles, des implémentations spécifiques de méthodes et de valeurs calculées peuvent être décrites
  • Les protocoles de classe permettent uniquement aux classes d'hériter

Comme vous le savez, tous les types simples (chaîne, int) dans Swift sont des structures. Dans la bibliothèque standard de Swift, cela ressemble par exemple à ceci:

public struct Int: FixedWidthInteger, SignedInteger { 

Dans le même temps, les types de collection (collection), à savoir tableau, ensemble, dictionnaire, peuvent également être intégrés dans le protocole, car ils sont également des structures. Par exemple, un dictionnaire est défini comme suit

 public struct Dictionary<Key, Value> where Key: Hashable { 

Habituellement, dans la programmation orientée objet, le concept de classes est utilisé, et tout le monde connaît le mécanisme d'héritage des méthodes et des variables de la classe parente de la classe descendante. En même temps, personne ne lui interdit de contenir des méthodes et des variables supplémentaires.

Dans le cas des protocoles, vous pouvez créer une hiérarchie de relations beaucoup plus intéressante. Pour décrire la classe suivante, vous pouvez utiliser plusieurs protocoles en même temps, ce qui vous permet de créer des conceptions assez complexes qui satisferont de nombreuses conditions en même temps. En revanche, les limitations des différents protocoles permettent de former un objet destiné uniquement à une application étroite et contenant un certain nombre de fonctions.

La mise en œuvre du protocole dans Swift est assez simple. La syntaxe implique un nom, un certain nombre de méthodes et des paramètres (variables) qu'il contiendra.

 protocol Employee { func work() var hours: Int { get } } 

De plus, dans Swift, les protocoles peuvent contenir non seulement les noms des méthodes, mais aussi leur implémentation. Le code de méthode dans le protocole est ajouté via des extensions. Dans la documentation, vous pouvez trouver de nombreuses mentions d'extensions, mais en ce qui concerne les protocoles dans les extensions, vous pouvez placer le nom de la fonction et le corps de la fonction.

 extension Employee { func work() { print ("do my job") } } 

Vous pouvez faire de même avec les variables.

 extension Employee { var hours: Int { return 8 } } 

Si nous utilisons quelque part l'objet associé au protocole, nous pouvons définir une variable avec une valeur fixe ou transmise. En fait, une variable est une petite fonction sans paramètres d'entrée ... ou avec la possibilité d'affecter directement un paramètre.

L'extension du protocole dans Swift vous permet d'implémenter le corps de la variable, puis en fait ce sera une valeur calculée - un paramètre calculé avec les fonctions get et set. Autrement dit, une telle variable ne stockera aucune valeur, mais jouera le rôle d'une ou plusieurs fonctions, ou jouera le rôle d'un proxy pour une autre variable.

Ou si nous prenons une classe ou une structure et implémentons le protocole, nous pouvons alors utiliser la variable habituelle:

 class Programmer { var hours: Int = 24 } extension Programmer: Employee { } 

Il convient de noter que les variables dans la définition du protocole ne peuvent pas être faibles. (faible est une implémentation de variation).

Il existe des exemples plus intéressants: vous pouvez implémenter l'extension du tableau et y ajouter une fonction liée au type de données du tableau. Par exemple, si le tableau contient des valeurs entières ou a le format équitable (adapté à la comparaison), la fonction peut, par exemple, comparer toutes les valeurs des cellules du tableau.

 extension Array where Element: Equatable {   var areAllElementsEqualToEachOther: Bool {       if isEmpty {           return false       }       var previousElement = self[0]       for (index, element) in self.enumerated() where index > 0 {           if element != previousElement {               return false           }           previousElement = element       }       return true   } } [1,1,1,1].areAllElementsEqualToEachOther 

Une petite remarque. Les variables et les fonctions des protocoles peuvent être statiques.

Utilisation de @ objc


La principale chose que vous devez savoir à ce sujet est que les protocoles @ objc Swift sont visibles dans le code Objective-C. A proprement parler, car ce «mot magique» @ objc existe. Mais tout le reste reste inchangé

 @objc protocol Typable {   @objc optional func test()   func type() } extension Typable {   func test() {       print("Extension test")   }   func type() {       print("Extension type")   } } class Typewriter: Typable {   func test() {       print("test")   }   func type() {       print("type")   } } 


Les protocoles de ce type ne peuvent être hérités que par des classes. Pour les listes et les structures, cela ne peut pas être fait.

C’est le seul moyen.
 @objc protocol Dummy { } class DummyClass: Dummy { } 


Il convient de noter que dans ce cas, il devient possible de définir des fonctions optionnelles (@obj option func), qui, si vous le souhaitez, peuvent ne pas être implémentées, comme pour la fonction test () dans l'exemple précédent. Mais des fonctions facultatives conditionnelles peuvent également être implémentées en étendant le protocole avec une implémentation vide.

 protocol Dummy { func ohPlease() } extension Dummy { func ohPlease() { } } 


Héritage de type


En créant une classe, une structure ou une énumération, nous pouvons désigner l'héritage d'un certain protocole - dans ce cas, les paramètres hérités fonctionneront pour notre classe et pour toutes les autres classes qui héritent de cette classe, même si nous n'y avons pas accès.

Soit dit en passant, dans ce contexte, un problème très intéressant apparaît. Disons que nous avons un protocole. Il y a de la classe. Et la classe implémente le protocole, et il a une fonction work (). Que se passe-t-il si nous avons une extension du protocole, qui a également une méthode work (). Laquelle sera appelée lorsque la méthode sera appelée?

 protocol Person {    func work() } extension Person {   func work() {       print("Person")   } } class Employee { } extension Employee: Person {   func work() {       print("Employee")   } } 

La méthode de classe sera lancée - ce sont les caractéristiques des méthodes de répartition dans Swift. Et cette réponse est donnée par de nombreux candidats. Mais sur la question de savoir comment s'assurer que le code n'a pas de méthode de classe, mais une méthode de protocole, seuls quelques-uns connaissent la réponse. Cependant, il existe également une solution pour cette tâche - cela implique de supprimer la fonction de la définition de protocole et d'appeler la méthode comme suit:

 protocol Person { //     func work() //      } extension Person {   func work() {       print("Person")   } } class Employee { } extension Employee: Person {   func work() {       print("Employee")   } } let person: Person = Employee() person.work() //output: Person 


Protocoles génériques


Swift possède également des protocoles génériques avec des types associés qui vous permettent de définir des variables de type. Un tel protocole peut se voir attribuer des conditions supplémentaires imposées aux types associatifs. Plusieurs de ces protocoles vous permettent de construire des structures complexes nécessaires à la formation de l'architecture d'application.

Cependant, vous ne pouvez pas implémenter une variable en tant que protocole générique. Il peut seulement être hérité. Ces constructions sont utilisées pour créer des dépendances dans les classes. Autrement dit, nous pouvons décrire une classe générique abstraite pour déterminer les types qui y sont utilisés.

 protocol Printer {   associatedtype PrintableClass: Hashable   func printSome(printable: PrintableClass) } extension Printer {   func printSome(printable: PrintableClass) {       print(printable.hashValue)   } } class TheIntPrinter: Printer {   typealias PrintableClass = Int } let intPrinter = TheIntPrinter() intPrinter.printSome(printable: 0) let intPrinterError: Printer = TheIntPrinter() //   

Il faut se rappeler que les protocoles génériques ont un niveau d'abstraction élevé. Par conséquent, dans les applications elles-mêmes, elles peuvent être redondantes. Mais en même temps, des protocoles génériques sont utilisés lors de la programmation des bibliothèques.

Protocoles de classe


Swift possède également des protocoles liés aux classes. Deux types de syntaxe sont utilisés pour les décrire.

 protocol Employee: AnyObject { } 

Ou

 protocol Employee: class { } 

Selon les développeurs du langage, l'utilisation de ces syntaxes est équivalente, mais le mot-clé class n'est utilisé qu'à cet endroit, contrairement à AnyObject, qui est un protocole.

Pendant ce temps, comme nous le voyons lors des entretiens, les gens ne peuvent souvent pas expliquer ce qu'est un protocole de classe et pourquoi il est nécessaire. Son essence réside dans le fait que nous avons la possibilité d'utiliser un objet, qui serait un protocole, et en même temps fonctionnerait comme un type de référence. Un exemple:

 protocol Handler: class {} class Presenter: Handler { weak var renderer: Renderer?  } protocol Renderer {} class View: Renderer { } 

Qu'est-ce que le sel?

IOS utilise la gestion de la mémoire en utilisant la méthode de comptage automatique des références, ce qui implique la présence de liens forts et faibles. Et dans certains cas, vous devez considérer lesquelles - les variables fortes (fortes) ou faibles (faibles) - sont utilisées dans les classes.

Le problème est que lors de l'utilisation d'un certain protocole comme type, lors de la description d'une variable (qui est un lien fort), un cycle de rétention peut se produire, entraînant des fuites de mémoire, car les objets seront maintenus partout par des liens forts. En outre, des problèmes peuvent survenir si vous décidez toujours d'écrire du code conformément aux principes de SOLID.

 protocol Handler {} class Presenter: Handler { var renderer: Renderer? } protocol Renderer {} class View: Renderer { var handler: Handler? } 

Pour éviter de telles situations, Swift utilise des protocoles de classe qui vous permettent de définir initialement des variables «faibles». Le protocole de classe vous permet de garder un objet une référence faible. Un exemple où cela vaut souvent la peine d'être considéré est appelé un délégué.

 protocol TableDelegate: class {} class Table {   weak var tableDelegate: TableDelegate? } 

Un autre exemple où les protocoles de classe doivent être utilisés est une indication explicite que l'objet est passé par référence.

Héritage et répartition de plusieurs méthodes


Comme indiqué au début de l'article, les protocoles peuvent être hérités plusieurs fois. Autrement dit,

 protocol Pet {   func waitingForItsOwner() } protocol Sleeper {   func sleepOnAChair() } class Kitty: Pet, Sleeper {   func eat() {       print("yammy")   }   func waitingForItsOwner() {       print("looking at the door")   }   func sleepOnAChair() {       print("dreams")   } } 

C'est utile, mais quels pièges sont cachés ici? Le fait est que des difficultés, du moins à première vue, surviennent en raison de l'envoi de méthodes (envoi de méthodes). En termes simples, il peut ne pas être clair quelle méthode sera appelée - le parent ou du type actuel.

Juste au-dessus, nous avons déjà couvert le sujet du fonctionnement du code; il appelle la méthode de classe. C'est, comme prévu.

 protocol Pet {   func waitingForItsOwner() } extension Pet { func waitingForItsOwner() { print("Pet is looking at the door") } } class Kitty: Pet { func waitingForItsOwner() { print("Kitty is looking at the door") } } let kitty: Pet = Kitty() kitty.waitingForItsOwner() // Output: Kitty is looking at the door 

Mais si vous essayez de supprimer la signature de la méthode de la définition du protocole, la «magie» se produit. En fait, c'est une question de l'interview: "Comment faire appeler une fonction à partir d'un protocole?"

 protocol Pet { } extension Pet { func waitingForItsOwner() { print("Pet is looking at the door") } } class Kitty: Pet { func waitingForItsOwner() { print("Kitty is looking at the door") } } let kitty: Pet = Kitty() kitty.waitingForItsOwner() // Output: Pet is looking at the door 

Mais si vous utilisez la variable non pas comme protocole, mais comme classe, alors tout ira bien.

 protocol Pet { } extension Pet { func waitingForItsOwner() { print("Pet is looking at the door") } } class Kitty: Pet { func waitingForItsOwner() { print("Kitty is looking at the door") } } let kitty = Kitty() kitty.waitingForItsOwner() // Output: Kitty is looking at the door 

Il s'agit de méthodes de répartition statiques lors de l'extension du protocole. Et cela doit être pris en compte. Et voici l'héritage multiple? Mais avec ceci: si vous prenez deux protocoles avec des fonctions implémentées, un tel code ne fonctionnera pas. Pour que la fonction soit exécutée, vous devrez effectuer un cast explicite vers le protocole souhaité. Tel est l'écho de l'héritage multiple de C ++.

 protocol Pet {   func waitingForItsOwner() } extension Pet {   func yawn() { print ("Pet yawns") } } protocol Sleeper {   func sleepOnAChair() } extension Sleeper {   func yawn() { print ("Sleeper yawns") } } class Kitty: Pet, Sleeper {   func eat() {       print("yammy")   }   func waitingForItsOwner() {       print("looking at the door")   }   func sleepOnAChair() {       print("dreams")   } } let kitty = Kitty() kitty.yawn() 

Une histoire similaire se produira si vous héritez d'un protocole d'un autre, où des fonctions sont implémentées dans des extensions. Le compilateur ne le laissera pas construire.

 protocol Pet {   func waitingForItsOwner() } extension Pet {   func yawn() { print ("Pet yawns") } } protocol Cat {   func walk() } extension Cat {   func yawn() { print ("Cat yawns") } } class Kitty:Cat {   func eat() {       print("yammy")   }   func waitingForItsOwner() {       print("looking at the door")   }   func sleepOnAChair() {       print("dreams")   } } let kitty = Kitty() 

Les deux derniers exemples montrent qu'il ne vaut pas la peine de remplacer complètement les protocoles par des classes. Vous pouvez vous perdre dans la planification statique.

Génériques et protocoles


On peut dire que c'est une question avec un astérisque, qui n'a pas du tout besoin d'être posée. Mais les codeurs aiment les constructions super abstraites, et bien sûr, quelques classes génériques inutiles doivent être dans le projet (où sans lui). Mais un programmeur ne serait pas programmeur s'il ne voulait pas tout résumer dans une autre abstraction. Et Swift, étant un langage jeune mais en développement dynamique, offre une telle opportunité, mais de manière limitée. (Oui, il ne s'agit pas de téléphones portables).

Premièrement, un test complet pour l'héritage possible n'est disponible que dans Swift 4.2, c'est-à-dire que ce n'est qu'à l'automne qu'il sera possible de l'utiliser normalement dans les projets. Sur Swift 4.1, un message apparaît que l'opportunité n'a pas encore été implémentée.

 protocol Property { } protocol PropertyConnection { } class SomeProperty { } extension SomeProperty: Property { } extension SomeProperty: PropertyConnection { } protocol ViewConfigurator { } protocol Connection { } class Configurator<T> where T: Property {   var property: T   init(property: T) {       self.property = property   } } extension Configurator: ViewConfigurator { } extension Configurator: Connection where T: PropertyConnection { } [Configurator(property: SomeProperty()) as ViewConfigurator]   .forEach { configurator in   if let connection = configurator as? Connection {       print(connection)   } } 

Pour Swift 4.1, les informations suivantes s'affichent:

 warning: Swift runtime does not yet support dynamically querying conditional conformance ('__lldb_expr_1.Configurator<__lldb_expr_1.SomeProperty>': '__lldb_expr_1.Connection') 

Alors que dans Swift 4.2, tout fonctionne comme prévu:

 __lldb_expr_5.Configurator<__lldb_expr_5.SomeProperty> connection 

Il convient également de noter que vous pouvez hériter du protocole avec un seul type de relation. S'il existe deux types de liens, l'héritage sera interdit au niveau du compilateur. Une explication détaillée de ce qui est et n'est pas possible est présentée ici .

 protocol ObjectConfigurator { } protocol Property { } class FirstProperty: Property { } class SecondProperty: Property { } class Configurator<T> where T: Property {   var properties: T   init(properties: T) {       self.properties = properties   } } extension Configurator: ObjectConfigurator where T == FirstProperty { } //   : // Redundant conformance of 'Configurator<T>' to protocol 'ObjectConfigurator' extension Configurator: ObjectConfigurator where T == SecondProperty { } 

Mais, malgré ces difficultés, travailler avec des connexions génériques est assez pratique.

Pour résumer


Les protocoles ont été fournis dans Swift dans sa forme actuelle pour rendre le développement plus structurel et fournir des modèles d'héritage plus avancés que dans le même Objective-C. Par conséquent, nous sommes convaincus que l'utilisation de protocoles est une décision justifiable, et assurez-vous de demander aux candidats aux développeurs ce qu'ils savent de ces éléments du langage Swift. Dans les articles suivants, nous aborderons les méthodes d'envoi.

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


All Articles