Heute setzen wir die Publikationsreihe zum Thema Mobile Development für iOS fort. Und wenn wir das letzte Mal bei Interviews darüber gesprochen haben, was Sie brauchen und was nicht, müssen wir in diesem Material auf das Thema Protokolle eingehen, das in Swift wichtig ist. Es geht darum, wie die Protokolle angeordnet sind, wie sie sich voneinander unterscheiden und wie sie mit den Objective-C-Schnittstellen kombiniert werden.

Wie bereits erwähnt, wird die neue Apple-Sprache weiterentwickelt, und die meisten Parameter und Funktionen sind in der Dokumentation deutlich angegeben. Aber wer liest die Dokumentation, wenn hier und jetzt Code geschrieben werden muss? Lassen Sie uns also die Hauptfunktionen der Swift-Protokolle direkt in unserem Beitrag durchgehen.
Zunächst sollte beachtet werden, dass Apple-Protokolle ein alternativer Begriff für das Konzept der „Schnittstelle“ sind, das in anderen Programmiersprachen verwendet wird. In Swift werden Protokolle verwendet, um Muster bestimmter Strukturen (sogenannte Blaupause) anzugeben, an denen auf abstrakter Ebene gearbeitet werden kann. In einfachen Worten definiert das Protokoll eine Reihe von Methoden und Variablen, die ein bestimmter Typ unbedingt erben muss.
Später in diesem Artikel werden die Momente nach und nach wie folgt offenbart: von einfachen und oft verwendeten zu komplexeren. Grundsätzlich können Sie bei Interviews Fragen in dieser Reihenfolge stellen, da sie das Kompetenzniveau des Bewerbers bestimmen - von der Juniorenstufe bis zur Seniorenstufe.
Welche Protokolle werden in Swift benötigt?
Mobile Entwickler verzichten häufig auf die Verwendung von Protokollen, verlieren jedoch die Fähigkeit, abstrakt mit einigen Entitäten zu arbeiten. Wenn wir die Hauptfunktionen der Protokolle in Swift hervorheben, erhalten wir die folgenden 7 Punkte:
- Protokolle bieten Mehrfachvererbung
- Protokolle können den Status nicht speichern
- Protokolle können von anderen Protokollen geerbt werden.
- Protokolle können auf Strukturen (struct), Klassen (class) und Enumerationen (enum) angewendet werden, um die Typfunktionalität zu definieren
- Mit generischen Protokollen können Sie komplexe Abhängigkeiten zwischen Typen und Protokollen während ihrer Vererbung angeben
- Protokolle definieren keine "starken" oder "schwachen" Variablenreferenzen
- In Erweiterungen der Protokolle können spezifische Implementierungen von Methoden und berechneten Werten beschrieben werden
- Klassenprotokolle erlauben nur Klassen zu erben
Wie Sie wissen, sind alle einfachen Typen (string, int) in Swift Strukturen. In der Swift-Standardbibliothek sieht dies beispielsweise folgendermaßen aus:
public struct Int: FixedWidthInteger, SignedInteger {
Gleichzeitig können auch Sammlungstypen (Sammlung), nämlich Array, Set, Dictionary, in das Protokoll gepackt werden, da es sich auch um Strukturen handelt. Ein Wörterbuch ist beispielsweise wie folgt definiert
public struct Dictionary<Key, Value> where Key: Hashable {
Normalerweise wird in der objektorientierten Programmierung das Konzept der Klassen verwendet, und jeder kennt den Mechanismus zum Erben von Methoden und Variablen der übergeordneten Klasse von der untergeordneten Klasse. Gleichzeitig verbietet ihm niemand, zusätzliche Methoden und Variablen zu enthalten.
Bei Protokollen können Sie eine viel interessantere Beziehungshierarchie erstellen. Um die nächste Klasse zu beschreiben, können Sie mehrere Protokolle gleichzeitig verwenden, wodurch Sie ziemlich komplexe Designs erstellen können, die viele Bedingungen gleichzeitig erfüllen. Andererseits ermöglichen die Einschränkungen verschiedener Protokolle die Bildung eines Objekts, das nur für eine enge Anwendung vorgesehen ist und eine bestimmte Anzahl von Funktionen enthält.
Die Implementierung des Protokolls in Swift ist recht einfach. Die Syntax impliziert einen Namen, eine Reihe von Methoden und Parameter (Variablen), die darin enthalten sein werden.
protocol Employee { func work() var hours: Int { get } }
Darüber hinaus können Protokolle in Swift nicht nur die Namen von Methoden enthalten, sondern auch deren Implementierung. Der Methodencode im Protokoll wird durch Erweiterungen hinzugefügt. In der Dokumentation finden Sie viele Erwähnungen von Erweiterungen, aber in Bezug auf Protokolle in Erweiterungen können Sie den Namen der Funktion und den Hauptteil der Funktion angeben.
extension Employee { func work() { print ("do my job") } }
Sie können dasselbe mit Variablen tun.
extension Employee { var hours: Int { return 8 } }
Wenn wir das dem Protokoll zugeordnete Objekt irgendwo verwenden, können wir eine Variable mit einem festen oder übertragenen Wert festlegen. Tatsächlich ist eine Variable eine kleine Funktion ohne Eingabeparameter ... oder mit der Möglichkeit, einen Parameter direkt zuzuweisen.
Wenn Sie das Protokoll in Swift erweitern, können Sie den Hauptteil der Variablen implementieren. Tatsächlich handelt es sich dann um einen berechneten Wert - einen berechneten Parameter mit den Funktionen get und set. Das heißt, eine solche Variable speichert keine Werte, sondern spielt entweder die Rolle einer Funktion oder von Funktionen oder spielt die Rolle eines Proxys für eine andere Variable.
Oder wenn wir eine Klasse oder Struktur nehmen und das Protokoll implementieren, können wir die übliche Variable darin verwenden:
class Programmer { var hours: Int = 24 } extension Programmer: Employee { }
Es ist erwähnenswert, dass die Variablen in der Protokolldefinition nicht schwach sein können. (schwach ist eine Variationsimplementierung).Es gibt weitere interessante Beispiele: Sie können die Erweiterung des Arrays implementieren und dort eine Funktion hinzufügen, die sich auf den Datentyp des Arrays bezieht. Wenn das Array beispielsweise ganzzahlige Werte enthält oder das Format vergleichbar ist (zum Vergleich geeignet), kann die Funktion beispielsweise alle Werte der Zellen des Arrays vergleichen.
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
Eine kleine Bemerkung. Variablen und Funktionen in Protokollen können statisch sein.
Verwenden von @
objc
Das Wichtigste, was Sie in dieser Angelegenheit wissen müssen, ist, dass
@
objc Swift-Protokolle im Objective-C-Code sichtbar sind. Genau genommen existiert für dieses „Zauberwort“
@
objc. Aber alles andere bleibt unverändert
@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") } }
Protokolle dieses Typs können nur von Klassen geerbt werden. Für Listings und Strukturen ist dies nicht möglich.
Das ist der einzige Weg.
@objc protocol Dummy { } class DummyClass: Dummy { }
Es ist anzumerken, dass es in diesem Fall möglich wird, optionale Funktionen (@obj optionale Funktion) zu definieren, die, falls gewünscht, nicht implementiert werden können, wie für die Funktion test () im vorherigen Beispiel. Bedingt optionale Funktionen können aber auch implementiert werden, indem das Protokoll mit einer leeren Implementierung erweitert wird.
protocol Dummy { func ohPlease() } extension Dummy { func ohPlease() { } }
Typvererbung
Durch Erstellen einer Klasse, Struktur oder Aufzählung können wir die Vererbung eines bestimmten Protokolls festlegen. In diesem Fall funktionieren die geerbten Parameter für unsere Klasse und für alle anderen Klassen, die diese Klasse erben, auch wenn wir keinen Zugriff darauf haben.
In diesem Zusammenhang tritt übrigens ein sehr interessantes Problem auf. Nehmen wir an, wir haben ein Protokoll. Es gibt eine Klasse. Die Klasse implementiert das Protokoll und hat eine work () -Funktion. Was passiert, wenn wir eine Erweiterung des Protokolls haben, die auch eine work () -Methode hat? Welches wird aufgerufen, wenn die Methode aufgerufen wird?
protocol Person { func work() } extension Person { func work() { print("Person") } } class Employee { } extension Employee: Person { func work() { print("Employee") } }
Die Klassenmethode wird gestartet - dies sind die Funktionen der Versandmethoden in Swift. Und diese Antwort wird von vielen Bewerbern gegeben. Auf die Frage, wie sichergestellt werden kann, dass der Code keine Klassenmethode, sondern eine Protokollmethode enthält, kennen nur wenige die Antwort. Es gibt jedoch auch eine Lösung für diese Aufgabe: Sie müssen die Funktion aus der Protokolldefinition entfernen und die Methode wie folgt aufrufen:
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
Generische Protokolle
Swift verfügt auch über generische Protokolle mit zugehörigen Typen, mit denen Sie Typvariablen definieren können. Einem solchen Protokoll können zusätzliche Bedingungen zugewiesen werden, die assoziativen Typen auferlegt werden. Mit mehreren dieser Protokolle können Sie komplexe Strukturen erstellen, die für die Bildung der Anwendungsarchitektur erforderlich sind.
Sie können eine Variable jedoch nicht als generisches Protokoll implementieren. Es kann nur vererbt werden. Diese Konstrukte werden verwendet, um Abhängigkeiten in Klassen zu erstellen. Das heißt, wir können eine abstrakte generische Klasse beschreiben, um die darin verwendeten Typen zu bestimmen.
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()
Es sollte beachtet werden, dass generische Protokolle einen hohen Abstraktionsgrad aufweisen. Daher können sie in den Anwendungen selbst redundant sein. Gleichzeitig werden beim Programmieren von Bibliotheken generische Protokolle verwendet.
Klassenprotokolle
Swift hat auch klassengebundene Protokolle. Zwei Arten von Syntax werden verwendet, um sie zu beschreiben.
protocol Employee: AnyObject { }
Oder
protocol Employee: class { }
Laut den Entwicklern der Sprache ist die Verwendung dieser Syntax äquivalent, aber das Schlüsselwort class wird im Gegensatz zu AnyObject, einem Protokoll, nur an dieser Stelle verwendet.
Wie wir in Interviews sehen, können die Leute oft nicht erklären, was ein Klassenprotokoll ist und warum es benötigt wird. Seine Essenz liegt in der Tatsache, dass wir die Möglichkeit bekommen, ein Objekt zu verwenden, das ein Protokoll wäre und gleichzeitig als Referenztyp fungieren würde. Ein Beispiel:
protocol Handler: class {} class Presenter: Handler { weak var renderer: Renderer? } protocol Renderer {} class View: Renderer { }
Was ist das Salz?
IOS verwendet die Speicherverwaltung mithilfe der automatischen Referenzzählmethode, bei der starke und schwache Verbindungen vorhanden sind. In einigen Fällen sollten Sie überlegen, welche - starken (starken) oder schwachen (schwachen) - Variablen in Klassen verwendet werden.
Das Problem besteht darin, dass bei Verwendung eines bestimmten Protokolls als Typ bei der Beschreibung einer Variablen (bei der es sich um eine starke Verbindung handelt) ein Aufbewahrungszyklus auftreten kann, der zu Speicherverlusten führt, da Objekte überall von starken Verbindungen gehalten werden. Außerdem können Probleme auftreten, wenn Sie sich weiterhin dazu entschließen, Code gemäß den Prinzipien von SOLID zu schreiben.
protocol Handler {} class Presenter: Handler { var renderer: Renderer? } protocol Renderer {} class View: Renderer { var handler: Handler? }
Um solche Situationen zu vermeiden, verwendet Swift Klassenprotokolle, mit denen Sie zunächst „schwache“ Variablen festlegen können. Mit dem Klassenprotokoll können Sie ein Objekt als schwache Referenz behalten. Ein Beispiel, bei dem dies häufig in Betracht gezogen werden sollte, wird als Delegierter bezeichnet.
protocol TableDelegate: class {} class Table { weak var tableDelegate: TableDelegate? }
Ein weiteres Beispiel, bei dem Klassenprotokolle verwendet werden sollten, ist ein expliziter Hinweis darauf, dass das Objekt als Referenz übergeben wird.
Vererbung und Versand mehrerer Methoden
Wie am Anfang des Artikels angegeben, können Protokolle mehrfach vererbt werden. Also,
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") } }
Das ist nützlich, aber welche Fallstricke sind hier verborgen? Die Sache ist, dass zumindest auf den ersten Blick Schwierigkeiten durch den Versand von Methoden entstehen (Methodenversand). In einfachen Worten ist möglicherweise nicht klar, welche Methode aufgerufen wird - die übergeordnete oder die aktuelle Methode.
Oben haben wir bereits das Thema behandelt, wie der Code funktioniert, er ruft die Klassenmethode auf. Das heißt, wie erwartet.
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()
Wenn Sie jedoch versuchen, die Methodensignatur aus der Protokolldefinition zu entfernen, tritt die „Magie“ auf. Tatsächlich ist dies eine Frage aus dem Interview: "Wie kann eine Funktion aus einem Protokoll aufgerufen werden?"
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()
Wenn Sie die Variable jedoch nicht als Protokoll, sondern als Klasse verwenden, ist alles in Ordnung.
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()
Bei der Erweiterung des Protokolls dreht sich alles um statische Dispatching-Methoden. Und das muss berücksichtigt werden. Und hier ist Mehrfachvererbung? Aber damit: Wenn Sie zwei Protokolle mit implementierten Funktionen verwenden, funktioniert ein solcher Code nicht. Damit die Funktion ausgeführt werden kann, müssen Sie das gewünschte Protokoll explizit umwandeln. Dies ist das Echo der Mehrfachvererbung von 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()
Ähnlich verhält es sich, wenn Sie ein Protokoll von einem anderen erben, bei dem Funktionen in Erweiterungen implementiert sind. Der Compiler lässt es nicht bauen.
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()
Die letzten beiden Beispiele zeigen, dass es sich nicht lohnt, die Protokolle vollständig durch Klassen zu ersetzen. Bei der statischen Planung können Sie verwirrt sein.
Generika und Protokolle
Wir können sagen, dass dies eine Frage mit einem Sternchen ist, die überhaupt nicht gestellt werden muss. Aber Programmierer lieben super-abstrakte Konstruktionen, und natürlich müssen ein paar unnötige generische Klassen im Projekt sein (wo ohne sie). Aber ein Programmierer wäre kein Programmierer, wenn er nicht alles in einer weiteren Abstraktion zusammenfassen wollte. Und Swift, eine junge, aber sich dynamisch entwickelnde Sprache, bietet eine solche Gelegenheit, aber auf begrenzte Weise. (Ja, hier geht es nicht um Mobiltelefone).
Erstens ist ein vollständiger Test auf mögliche Vererbung nur in Swift 4.2 möglich, dh nur im Herbst kann dieser Test normalerweise in Projekten verwendet werden. In Swift 4.1 wird die Meldung angezeigt, dass die Opportunity noch nicht implementiert wurde.
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) } }
Für Swift 4.1 wird Folgendes angezeigt:
warning: Swift runtime does not yet support dynamically querying conditional conformance ('__lldb_expr_1.Configurator<__lldb_expr_1.SomeProperty>': '__lldb_expr_1.Connection')
Während in Swift 4.2 alles wie erwartet funktioniert:
__lldb_expr_5.Configurator<__lldb_expr_5.SomeProperty> connection
Es ist auch erwähnenswert, dass Sie das Protokoll mit nur einem Beziehungstyp erben können. Wenn es zwei Arten von Links gibt, ist die Vererbung auf Compilerebene verboten. Eine detaillierte Erklärung dessen, was möglich ist und was nicht, wird
hier gezeigt.
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 { }
Trotz dieser Schwierigkeiten ist die Arbeit mit Verbindungen in Generika recht praktisch.
Zusammenfassend
In Swift wurden in der aktuellen Form Protokolle bereitgestellt, um die Entwicklung struktureller zu gestalten und fortgeschrittenere Vererbungsmodelle bereitzustellen als in demselben Objective-C. Wir sind daher zuversichtlich, dass die Verwendung von Protokollen gerechtfertigt ist, und fragen Sie die Kandidaten für Entwickler, was sie über diese Elemente der Swift-Sprache wissen. In den folgenden Beiträgen werden wir auf Versandmethoden eingehen.