Hintergrund:
Die Protokollabstraktion wurde in Swift eingeführt, um gegenüber der klassischen Vererbung von Objekten aus der OOP zu konkurrieren. Während in Objective-C und anderen OOP-Sprachen der Ansatz der Vererbung "von allgemein zu spezifisch" dominierte, förderte Swift von Anfang an Protokolle als primären Weg zur Erlangung von Abstraktion, wobei die Komposition über die Vererbung gestellt wurde.
Problem:
Klassische Vererbung impliziert eine strenge Hierarchie: einen Baum von Unterklassen mit notwendiger Erweiterung durch Override. Dies schränkt die Flexibilität ein, führt zu "anfälligem" Code, erschwert das Refactoring und führt zur Aufblähung von Basisklassen. Außerdem unterstützt Swift keine multiple Vererbung von Klassen – das bedeutet, dass Wiederverwendung von Funktionalität nur durch andere Mechanismen möglich ist.
Lösung:
Die Protokollabstraktion ermöglicht es, ein "Set von Anforderungen" zu deklarieren, die ein Typ erfüllen muss. Protokolle können erweitert werden (extension), um allgemeine Logik einzuführen, was sie dem Konzept der "Mixins" näher bringt:
Beispielcode:
protocol Drawable { func draw() } extension Drawable { func draw() { print("Default drawing") } } struct Circle: Drawable {} let c = Circle() c.draw() // Gibt "Default drawing" aus
Schlüsseleigenschaften:
Was ist der Unterschied zwischen einer Protokollerweiterung und einer normalen Implementierung in einer Klasse?
Die Protokollerweiterung über extension fügt eine Standardimplementierung nur für die Fälle hinzu, in denen der Benutzer diese Methode nicht in seinem Typ implementiert hat. Wenn die Methode explizit im Typ implementiert ist, wird genau diese aufgerufen.
Beispiel:
protocol Demo { func foo() } extension Demo { func foo() { print("default") } } struct X: Demo { func foo() { print("custom") } } X().foo() // "custom"
Was passiert, wenn man auf einen Typ, der ein Protokoll implementiert und dessen Erweiterung aufruft, als Daten des Protokolls zugreift?
Wenn das Protokoll eine Methode als erforderlich deklariert (Anforderung), wird die Implementierung des konkreten Typs verwendet, selbst wenn auf den Typ des Protokolls umgewandelt wird. Wenn jedoch in der Erweiterung ein neues (früher nicht im Protokoll deklariertes) Attribut hinzugefügt wird, ist es nur über die Erweiterung verfügbar, nicht über den Protokolltyp.
Kann man in einem Array Instanzen unterschiedlicher structs speichern, die ein Protokoll implementieren?
Ja – dank "existential" Typen (zum Beispiel [Drawable]) können heterogene Sammlungen gespeichert werden:
struct Tri: Drawable { func draw() { print("Triangle") } } let arr: [Drawable] = [Circle(), Tri()] arr.forEach { $0.draw() }
In der Firma gab es eine Basissuperklasse Shape, von der alle Figuren (Circle, Square, Polygon) abgeleitet wurden. Der Basisklasse wuchs, weil jede neue Figur über Override unterstützt werden musste. Das Erweitern des Systems wurde immer schwieriger – jeder neue Typ brach ABI und erzwang das Neuschreiben des bestehenden Codes.
Vorteile:
Nachteile:
Es wurden mehrere Protokolle verwendet: Drawable, Colorable, Animatable. Jetzt kann jede Figur gleichzeitig "animierbar und farbig" gemacht werden, ohne die anderen Strukturen zu ändern. Neue Funktionalitäten werden über Erweiterungen hinzugefügt.
Vorteile:
Nachteile: