Protokollorientierte Programmierung in Swift 5.1

Protokolle sind eine grundlegende Eigenschaft von Swift. Sie spielen eine wichtige Rolle in Standard-Swift-Bibliotheken und sind eine gängige Methode zur Codierung der Abstraktion. In vielerlei Hinsicht ähneln sie Schnittstellen in anderen Programmiersprachen.

In diesem Tutorial stellen wir Ihnen einen Anwendungsentwicklungsansatz vor, der als protokollorientierte Programmierung bezeichnet wird und fast zum Kern von Swift geworden ist. Dies ist wirklich das, was Sie verstehen müssen, wenn Sie Swift lernen!

In diesem Handbuch haben Sie:

  • den Unterschied zwischen objektorientierter und protokollorientierter Programmierung verstehen;
  • die Standardprotokollimplementierungen verstehen;
  • Erfahren Sie, wie Sie die Funktionalität der Standard-Swift-Bibliothek erweitern können.
  • Erfahren Sie, wie Sie Protokolle mit Generika erweitern.

Loslegen


Stellen Sie sich vor, Sie entwickeln ein Rennspiel. Ihre Spieler können Autos, Motorräder und Flugzeuge fahren. Und selbst wenn man auf Vögeln fliegt, ist das ein Spiel, oder? Die Hauptsache hier ist, dass es eine Dofiga aller Arten von „Dingen“ gibt, auf denen man fahren, fliegen usw. kann.

Ein üblicher Ansatz zur Entwicklung solcher Anwendungen ist die objektorientierte Programmierung. In diesem Fall schließen wir die gesamte Logik in einige Basisklassen ein, von denen wir danach erben. Basisklassen müssen daher die Logik von "Blei" und "Pilot" enthalten.

Wir beginnen mit der Entwicklung, indem wir Klassen für jedes Fahrzeug erstellen. Wir werden die "Vögel" auf später verschieben, wir werden etwas später zu ihnen zurückkehren.

Wir sehen das Auto und MotorradDie Funktionalität ist sehr ähnlich, daher erstellen wir die Basisklasse MotorVehicle . Auto und Motorrad werden von MotorVehicle geerbt. Auf die gleiche Weise erstellen wir die Aircraft- Basisklasse , aus der wir die Plane- Klasse erstellen .

Sie denken, dass alles in Ordnung ist, aber - bam! - Die Aktion Ihres Spiels findet im 20. Jahrhundert statt und einige Autos können bereits fliegen .

Also hatten wir einen Knebel. Swift hat keine Mehrfachvererbung. Wie können Ihre fliegenden Autos sowohl von Motorfahrzeugen als auch von Flugzeugen geerbt werden? Eine weitere Basisklasse erstellen, die beide Funktionen kombiniert? Höchstwahrscheinlich nicht, da es keinen klaren und einfachen Weg gibt, dies zu erreichen.

Und was wird unser Spiel in dieser schrecklichen Situation retten? Protokollorientierte Programmierung hat es eilig zu helfen!

Wie ist protokollorientierte Programmierung?


Mit Protokollen können Sie ähnliche Methoden, Funktionen und Eigenschaften für Klassen, Strukturen und Aufzählungen gruppieren. In nur Klassen können Sie jedoch die Vererbung von der Basisklasse verwenden.

Der Vorteil von Protokollen in Swift besteht darin, dass ein Objekt mehreren Protokollen entsprechen kann.

Wenn Sie diese Methode verwenden, wird Ihr Code modularer. Stellen Sie sich Protokolle als Bausteine ​​der Funktionalität vor. Wenn Sie einem Objekt neue Funktionen hinzufügen und es an ein bestimmtes Protokoll anpassen, erstellen Sie kein völlig neues Objekt von Grund auf neu. Es wäre zu lang. Stattdessen fügen Sie verschiedene Bausteine ​​hinzu, bis das Objekt fertig ist.

Der Wechsel von einer Basisklasse zu Protokollen löst unser Problem. Mit Protokollen können wir eine FlyingCar-Klasse erstellen, die sowohl MotorVehicle als auch Aircraft entspricht. Schön, was?

Lass uns den Code machen


Führen Sie Xcode aus, erstellen Sie einen Spielplatz, speichern Sie ihn als SwiftProtocols.playground und fügen Sie diesen Code hinzu:

protocol Bird {
  var name: String { get }
  var canFly: Bool { get }
}

protocol Flyable {
  var airspeedVelocity: Double { get }
}


Kompilieren Sie mit Command-Shift-Return , um sicherzustellen, dass alles in Ordnung ist.

Hier definieren wir ein einfaches Bird- Protokoll mit den Eigenschaften name und canFly . Dann haben wir das definieren Flyable Protokoll mit der airspeedVelocity Eigenschaft .

In der „Vorprotokoll-Ära“ begann ein Entwickler mit der Flyable-Klasse als Basisklasse und definierte dann mithilfe der Vererbung Bird und alles andere, was fliegen könnte.

Bei der protokollorientierten Programmierung beginnt alles mit einem Protokoll. Mit dieser Technik können wir eine Skizze einer Funktion ohne Basisklasse kapseln.

Wie Sie jetzt sehen werden, macht dies den Schriftentwurfsprozess viel flexibler.

Bestimmen Sie den Typ, der dem Protokoll entspricht


Fügen Sie diesen Code unten auf dem Spielplatz hinzu:

struct FlappyBird: Bird, Flyable {
  let name: String
  let flappyAmplitude: Double
  let flappyFrequency: Double
  let canFly = true

  var airspeedVelocity: Double {
    3 * flappyFrequency * flappyAmplitude
  }
}


Dieser Code definiert eine neue FlappyBird- Struktur , die sowohl dem Bird-Protokoll als auch dem Flyable-Protokoll entspricht. Die Eigenschaft airspeedVelocity ist eine Arbeit von Flappy Frequency und Flappy Amplitude. Die Eigenschaft canFly gibt true zurück.

Fügen Sie nun die Definitionen von zwei weiteren Strukturen hinzu:

struct Penguin: Bird {
  let name: String
  let canFly = false
}

struct SwiftBird: Bird, Flyable {
  var name: String { "Swift \(version)" }
  let canFly = true
  let version: Double
  private var speedFactor = 1000.0
  
  init(version: Double) {
    self.version = version
  }

  // Swift is FASTER with each version!
  var airspeedVelocity: Double {
    version * speedFactor
  }
}


Pinguin ist ein Vogel, kann aber nicht fliegen. Es ist gut, dass wir keine Vererbung verwenden und nicht alle Vögel flugfähig gemacht haben !

Wenn Sie Protokolle verwenden, definieren Sie die Komponenten der Funktion und stellen sicher , dass alle geeigneten Objekte dem Protokoll entsprechen.

Dann definieren wir SwiftBird , aber in unserem Spiel gibt es verschiedene Versionen davon. Je größer die Versionsnummer ist, desto größer ist die Fluggeschwindigkeit , die als berechnete Eigenschaft definiert ist.

Es gibt jedoch eine gewisse Redundanz. Jeder Vogeltyp muss eine explizite Definition der canFly-Eigenschaft definieren, obwohl wir eine Definition des Flyable-Protokolls haben. Es sieht so aus, als müssten wir die Standardimplementierung von Protokollmethoden bestimmen. Nun, dafür gibt es Protokollerweiterungen.

Erweitern des Protokolls mit Standardverhalten


Mit Protokollerweiterungen können Sie das Standardprotokollverhalten festlegen. Schreiben Sie diesen Code unmittelbar nach der Definition des Bird-Protokolls:

extension Bird {
  // Flyable birds can fly!
  var canFly: Bool { self is Flyable }
}


Dieser Code definiert die Erweiterung des Bird- Protokolls . Diese Erweiterung bestimmt, dass die Eigenschaft canFly true zurückgibt , wenn der Typ dem Flyable- Protokoll entspricht . Mit anderen Worten, jeder Flyable-Vogel muss canFly nicht mehr explizit einstellen.

Entfernen Sie nun let canFly = ... aus den Definitionen von FlappyBird, Penguin und SwiftBird. Kompilieren Sie den Code und stellen Sie sicher, dass alles in Ordnung ist.

Lassen Sie uns die Transfers machen


Aufzählungen in Swift können Protokollen entsprechen. Fügen Sie die folgende Aufzählungsdefinition hinzu:

enum UnladenSwallow: Bird, Flyable {
  case african
  case european
  case unknown
  
  var name: String {
    switch self {
    case .african:
      return "African"
    case .european:
      return "European"
    case .unknown:
      return "What do you mean? African or European?"
    }
  }
  
  var airspeedVelocity: Double {
    switch self {
    case .african:
      return 10.0
    case .european:
      return 9.9
    case .unknown:
      fatalError("You are thrown from the bridge of death!")
    }
  }
}


Durch die Definition der entsprechenden Eigenschaften entspricht UnladenSwallow zwei Protokollen - Bird und Flyable. Auf diese Weise wird die Standarddefinition für canFly implementiert.

Überschreiben des Standardverhaltens


Unser Typ UnladenSwallow erhielt gemäß dem Bird- Protokoll automatisch eine Implementierung für canFly . Wir benötigen jedoch UnladenSwallow.unknown, um false für canFly zurückzugeben.

Fügen Sie den folgenden Code hinzu:

extension UnladenSwallow {
  var canFly: Bool {
    self != .unknown
  }
}

Jetzt werden nur .african und .european für canFly wahr zurückkehren. Hör zu! Fügen Sie den folgenden Code unten auf unserem Spielplatz hinzu:

UnladenSwallow.unknown.canFly         // false
UnladenSwallow.african.canFly         // true
Penguin(name: "King Penguin").canFly  // false

Stellen Sie den Spielplatz zusammen und überprüfen Sie die empfangenen Werte anhand der in den obigen Kommentaren angegebenen Werte.

Daher definieren wir Eigenschaften und Methoden ähnlich wie virtuelle Methoden in der objektorientierten Programmierung neu.

Protokoll erweitern


Sie können Ihr eigenes Protokoll auch an ein anderes Protokoll aus der Swift-Standardbibliothek anpassen und das Standardverhalten definieren. Ersetzen Sie die Bird-Protokolldeklaration durch den folgenden Code:

protocol Bird: CustomStringConvertible {
  var name: String { get }
  var canFly: Bool { get }
}

extension CustomStringConvertible where Self: Bird {
  var description: String {
    canFly ? "I can fly" : "Guess I'll just sit here :["
  }
}

Die Einhaltung des CustomStringConvertible-Protokolls bedeutet, dass Ihr Typ eine Beschreibungseigenschaft haben muss. Anstatt diese Eigenschaft dem Bird-Typ und all seinen Ableitungen hinzuzufügen, definieren wir die Protokollerweiterung CustomStringConvertible, die nur dem Bird-Typ zugeordnet wird.

Geben Sie UnladenSwallow.african am unteren Rand des Spielplatzes. Kompilieren Sie und Sie werden sehen, dass ich fliegen kann.

Protokolle in Swift Standard Libraries


Wie Sie sehen können, sind Protokolle eine effektive Möglichkeit, Typen zu erweitern und anzupassen. In der Swift-Standardbibliothek wird diese Eigenschaft ebenfalls häufig verwendet.

Fügen Sie diesen Code dem Spielplatz hinzu:

let numbers = [10, 20, 30, 40, 50, 60]
let slice = numbers[1...3]
let reversedSlice = slice.reversed()

let answer = reversedSlice.map { $0 * 10 }
print(answer)

Sie wissen wahrscheinlich, was dieser Code ausgeben wird, aber Sie werden möglicherweise überrascht sein, welche Typen hier verwendet werden.

Slice ist beispielsweise nicht Array, sondern ArraySlice. Dies ist ein spezieller „Wrapper“, der eine effiziente Möglichkeit bietet, mit Teilen des Arrays zu arbeiten. Dementsprechend ist reversedSlice eine ReversedCollection <ArraySlice>.

Glücklicherweise ist die Kartenfunktion als Erweiterung des Sequenzprotokolls definiert, das allen Sammlungstypen entspricht. Dies ermöglicht es uns, die Map-Funktion sowohl auf Array als auch auf ReversedCollection anzuwenden und den Unterschied nicht zu bemerken. Bald werden Sie diesen nützlichen Trick nutzen.

Auf die Plätze


Bisher haben wir verschiedene Typen identifiziert, die dem Bird-Protokoll entsprechen. Jetzt werden wir etwas ganz anderes hinzufügen:

class Motorcycle {
  init(name: String) {
    self.name = name
    speed = 200.0
  }

  var name: String
  var speed: Double
}

Dieser Typ hat nichts mit Vögeln und Flügen zu tun. Wir wollen ein Motorradrennen mit Pinguinen veranstalten. Es ist Zeit, diese seltsame Firma an den Start zu bringen.

Alles zusammenfügen


Um solche unterschiedlichen Rennfahrer irgendwie zu vereinen, brauchen wir ein gemeinsames Protokoll für den Rennsport. Wir können all dies tun, ohne alle zuvor erstellten Typen zu berühren, mit Hilfe einer wunderbaren Sache, die als rückwirkende Modellierung bezeichnet wird. Fügen Sie dies einfach dem Spielplatz hinzu:

// 1
protocol Racer {
  var speed: Double { get }  // speed is the only thing racers care about
}

// 2
extension FlappyBird: Racer {
  var speed: Double {
    airspeedVelocity
  }
}

extension SwiftBird: Racer {
  var speed: Double {
    airspeedVelocity
  }
}

extension Penguin: Racer {
  var speed: Double {
    42  // full waddle speed
  }
}

extension UnladenSwallow: Racer {
  var speed: Double {
    canFly ? airspeedVelocity : 0.0
  }
}

extension Motorcycle: Racer {}

// 3
let racers: [Racer] =
  [UnladenSwallow.african,
   UnladenSwallow.european,
   UnladenSwallow.unknown,
   Penguin(name: "King Penguin"),
   SwiftBird(version: 5.1),
   FlappyBird(name: "Felipe", flappyAmplitude: 3.0, flappyFrequency: 20.0),
   Motorcycle(name: "Giacomo")]

Folgendes tun wir hier: Definieren Sie zunächst das Racer-Protokoll. Dies ist alles, was an Rennen teilnehmen kann. Dann haben wir alle unsere zuvor erstellten Typen in das Racer-Protokoll umgewandelt. Und schließlich erstellen wir ein Array, das Instanzen von jedem unserer Typen enthält.

Stellen Sie den Spielplatz so zusammen, dass alles in Ordnung ist.

Höchstgeschwindigkeit


Wir schreiben eine Funktion, um die Höchstgeschwindigkeit der Fahrer zu bestimmen. Fügen Sie diesen Code am Ende des Spielplatzes hinzu:

func topSpeed(of racers: [Racer]) -> Double {
  racers.max(by: { $0.speed < $1.speed })?.speed ?? 0.0
}

topSpeed(of: racers) // 5100

Hier verwenden wir die Max-Funktion, um den Fahrer mit maximaler Geschwindigkeit zu finden und zurückzugeben. Wenn das Array leer ist, wird 0.0 zurückgegeben.

Die Funktion allgemeiner gestalten


Angenommen, das Racers-Array ist groß genug, und wir müssen die maximale Geschwindigkeit nicht im gesamten Array, sondern in einem Teil davon ermitteln. Die Lösung besteht darin, topSpeed ​​(von :) so zu ändern, dass nicht speziell ein Array, sondern alles, was dem Sequenzprotokoll entspricht, als Argument verwendet wird.

Ersetzen Sie unsere Implementierung von topSpeed ​​(von :) wie folgt:

// 1
func topSpeed<RacersType: Sequence>(of racers: RacersType) -> Double
    /*2*/ where RacersType.Iterator.Element == Racer {
  // 3
  racers.max(by: { $0.speed < $1.speed })?.speed ?? 0.0
}

  1. RacersType ist der generische Argumenttyp unserer Funktion. Es kann alles sein, was dem Sequenzprotokoll entspricht.
  2. Dabei wird festgelegt, dass der Sequenzinhalt dem Racer-Protokoll entsprechen muss.
  3. Der Funktionskörper selbst bleibt unverändert.

Überprüfen Sie dies, indem Sie dies am Ende unseres Spielplatzes hinzufügen:

topSpeed(of: racers[1...3]) // 42

Jetzt funktioniert unsere Funktion mit jedem Typ, der das Sequenzprotokoll erfüllt, einschließlich ArraySlice.

Die Funktion „schneller“ machen


Geheimnis: Du kannst es noch besser machen. Fügen Sie dies ganz unten hinzu:

extension Sequence where Iterator.Element == Racer {
  func topSpeed() -> Double {
    self.max(by: { $0.speed < $1.speed })?.speed ?? 0.0
  }
}

racers.topSpeed()        // 5100
racers[1...3].topSpeed() // 42

Und jetzt haben wir das Sequenzprotokoll selbst mit topSpeed ​​() erweitert. Dies gilt nur, wenn Sequence den Racer-Typ enthält.

Protokollkomparatoren


Ein weiteres Merkmal von Swift-Protokollen ist die Definition der Gleichheitsoperatoren von Objekten oder deren Vergleich. Wir schreiben folgendes:

protocol Score {
  var value: Int { get }
}

struct RacingScore: Score {
  let value: Int
}

Mit dem Score-Protokoll können Sie Code schreiben, der alle Elemente dieses Typs auf eine Weise behandelt. Wenn Sie jedoch einen ganz bestimmten Typ wie RacingScore erhalten, werden Sie ihn nicht mit anderen Ableitungen des Score-Protokolls verwechseln.

Wir möchten, dass die Punktzahlen verglichen werden, um festzustellen, wer die höchste Punktzahl hat. Vor Swift 3 mussten Entwickler globale Funktionen schreiben, um einen Operator für ein Protokoll zu definieren. Jetzt können wir diese statischen Methoden im Modell selbst definieren. Dazu ersetzen wir die Definitionen von Score und RacingScore wie folgt:

protocol Score: Comparable {
  var value: Int { get }
}

struct RacingScore: Score {
  let value: Int
  
  static func <(lhs: RacingScore, rhs: RacingScore) -> Bool {
    lhs.value < rhs.value
  }
}

Wir haben die gesamte Logik für RacingScore an einem Ort. Für das Protokoll "Vergleichbar" müssen Sie eine Implementierung nur für die Funktion "weniger als" definieren. Alle anderen Vergleichsfunktionen werden automatisch implementiert, basierend auf der Implementierung der von uns erstellten Funktion "kleiner als".

Testen:

RacingScore(value: 150) >= RacingScore(value: 130) // true

Änderungen am Objekt vornehmen


Bisher hat jedes Beispiel gezeigt, wie Funktionen hinzugefügt werden können. Aber was ist, wenn wir ein Protokoll erstellen möchten, das etwas in einem Objekt ändert ? Dies kann mithilfe von Mutationsmethoden in unserem Protokoll erfolgen.

Fügen Sie ein neues Protokoll hinzu:

protocol Cheat {
  mutating func boost(_ power: Double)
}

Hier definieren wir ein Protokoll, das es uns ermöglicht zu betrügen. Auf welche Weise? Beliebige Änderung des Boost-Inhalts.

Erstellen Sie nun eine SwiftBird-Erweiterung, die dem Cheat-Protokoll entspricht:

extension SwiftBird: Cheat {
  mutating func boost(_ power: Double) {
    speedFactor += power
  }
}

Hier implementieren wir die Funktion boost (_ :) und erhöhen den speedFactor um den übertragenen Wert. Das mutierende Schlüsselwort macht die Struktur verständlich, dass einer ihrer Werte durch diese Funktion geändert wird.

Hör zu!
var swiftBird = SwiftBird(version: 5.0)
swiftBird.boost(3.0)
swiftBird.airspeedVelocity // 5015
swiftBird.boost(3.0)
swiftBird.airspeedVelocity // 5030

Fazit


Hier können Sie den vollständigen Quellcode für den Spielplatz herunterladen.

Sie haben die Möglichkeiten der protokollorientierten Programmierung kennengelernt, indem Sie einfache Protokolle erstellt und deren Funktionen mithilfe von Erweiterungen erweitert haben. Mit der Standardimplementierung geben Sie den Protokollen das entsprechende „Verhalten“. Fast wie bei Basisklassen, aber nur besser, da dies alles auch für Strukturen und Aufzählungen gilt.

Sie haben auch gesehen, dass die Protokollerweiterung für die zugrunde liegenden Swift-Protokolle gilt.

Hier finden Sie den offiziellen Protokollführer .

Sie können sich auch eine hervorragende WWDC-Vorlesung über protokollorientierte Programmierung ansehen .

Wie bei jedem Programmierparadigma besteht die Gefahr, dass Sie mitgerissen werden und die Protokolle links und rechts verwenden. Hier ist ein interessanter Hinweis zu den Gefahren von Entscheidungen im Stil von Silberkugeln.

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


All Articles