Verwendung von Generics in Swift

Mit Generics können Sie eine Variable deklarieren, die bei der Ausführung einer von uns definierten Gruppe von Typen zugewiesen werden kann.

In Swift kann ein Array Daten jeden Typs enthalten. Wenn wir ein Array mit Ganzzahlen, Strings oder Floats benötigen, können wir mit der Swift-Standardbibliothek ein Array erstellen. Der Typ, den das Array enthalten soll, wird bei der Deklaration definiert. Arrays sind ein bekanntes Beispiel für Generika. Wenn Sie Ihre eigene Sammlung implementieren möchten, möchten Sie auf jeden Fall Generika verwenden. 

Lassen Sie uns die Generika untersuchen und was für großartige Dinge sie zulassen.

1. Generische Funktionen

Wir beginnen mit der Erstellung einer einfachen generischen Funktion. Unser Ziel ist es, eine Funktion zu erstellen, um zu prüfen, ob zwei Objekte vom selben Typ sind. Wenn sie vom selben Typ sind, machen wir den Wert des zweiten Objekts gleich dem Wert des ersten Objekts. Wenn sie nicht vom selben Typ sind, drucken wir "nicht den gleichen Typ". Hier ist ein Versuch, eine solche Funktion in Swift zu implementieren.

func sameType (one: Int, inout two: Int) -> Void // Dies ist immer wahr, wenn (one.dynamicType == two.dynamicType) two = one else print ("nicht gleicher Typ") 

In einer Welt ohne Generika stoßen wir auf ein großes Problem. Bei der Definition einer Funktion müssen wir den Typ jedes Arguments angeben. Wenn wir möchten, dass unsere Funktion mit jedem möglichen Typ arbeitet, müssten wir eine Definition unserer Funktion mit unterschiedlichen Parametern für jede mögliche Kombination von Typen schreiben. Das ist keine gangbare Option.

func sameType (one: Int, inout two: String) -> Void // Dies wäre immer false, wenn (one.dynamicType == two.dynamicType) two = one else print ("nicht gleicher Typ") 

Wir können dieses Problem vermeiden, indem wir Generika verwenden. Schauen Sie sich das folgende Beispiel an, in dem wir Generika nutzen.

func sameType(one: T, inout two: E) -> Void if (one.dynamicType == two.dynamicType) two = one else print ("nicht gleicher Typ")

Hier sehen wir die Syntax der Verwendung von Generika. Die generischen Typen werden durch symbolisiert T und E. Die Typen werden durch Put angegeben in unserer Funktionsdefinition nach dem Namen der Funktion. Denk an T und E als Platzhalter für welchen Typ wir unsere Funktion verwenden.

Es gibt jedoch ein großes Problem mit dieser Funktion. Es wird nicht kompiliert. Der Compiler gibt einen Fehler aus, der darauf hinweist T ist nicht konvertierbar in E. Generika gehen davon aus, dass seit T und E unterschiedliche Bezeichnungen haben, werden sie auch unterschiedlich sein. Dies ist in Ordnung, wir können unser Ziel immer noch mit zwei Definitionen unserer Funktion erreichen.

func sameType(Eins: T, Inout Zwei: E) -> Void print ("nicht gleicher Typ") func sameType(Eins: T, inout zwei: T) -> Leere zwei = Eins

Es gibt zwei Fälle für die Argumente unserer Funktion:

  • Wenn sie vom selben Typ sind, wird die zweite Implementierung aufgerufen. Der Wert von zwei wird dann zugewiesen ein.
  • Wenn es sich um unterschiedliche Typen handelt, wird die erste Implementierung aufgerufen und die Zeichenfolge "Nicht derselbe Typ" wird auf die Konsole gedruckt. 

Wir haben unsere Funktionsdefinitionen von einer potenziell unendlichen Anzahl von Argumenttypkombinationen auf nur zwei reduziert. Unsere Funktion funktioniert jetzt mit jeder beliebigen Kombination von Typen als Argumente.

var s = "apple" var p = 1 sameType (2, zwei: & p) print (p) sameType ("apple", zwei: & p) // Ausgabe: 1 "ungleicher Typ"

Die generische Programmierung kann auch auf Klassen und Strukturen angewendet werden. Lassen Sie uns einen Blick darauf werfen, wie das funktioniert.

2. Generische Klassen und Strukturen

Betrachten wir die Situation, in der wir unseren eigenen Datentyp erstellen möchten, einen binären Baum. Wenn wir einen traditionellen Ansatz verwenden, bei dem wir keine Generics verwenden, würden wir einen binären Baum erstellen, der nur einen Datentyp enthalten kann. Zum Glück haben wir Generika.

Ein binärer Baum besteht aus Knoten mit:

  • zwei Kinder oder Zweige, die andere Knoten sind
  • ein Datenelement, das das generische Element darstellt
  • ein übergeordneter Knoten, der normalerweise nicht vom Knoten referenziert wird

Jeder binäre Baum hat einen Kopfknoten, der keine Eltern hat. Die zwei Kinder werden üblicherweise als linker und rechter Knoten unterschieden.

Alle Daten in einem linken untergeordneten Element müssen kleiner sein als der übergeordnete Knoten. Alle Daten im rechten untergeordneten Element müssen größer sein als der übergeordnete Knoten.

Klasse BTree  var Daten: T? = nil var übrig: BTree? = nil var rechts: BTree? = nil func insert (newData: T) if (self.data> newData) // In den linken Teilbaum einfügen else if (self.data < newData)  // Insert into right subtree  else if (self.data == nil)  self.data = newData return   

Die Erklärung des BTree class deklariert auch das generische T, was durch die beschränkt ist Vergleichbar Protokoll. Wir werden die Protokolle und Einschränkungen in Kürze besprechen.

Das Datenelement unseres Baums wird als Typ angegeben T. Jedes eingefügte Element muss ebenfalls vom Typ sein T wie in der Erklärung des einfügen(_:) Methode. Bei einer generischen Klasse wird der Typ angegeben, wenn das Objekt deklariert wird.

var Baum: BTree

In diesem Beispiel erstellen wir einen binären Baum von Ganzzahlen. Eine generische Klasse zu erstellen ist ziemlich einfach. Alles, was wir tun müssen, ist, das Generikum in die Deklaration aufzunehmen und es im Text gegebenenfalls zu referenzieren.

3. Protokolle und Einschränkungen

In vielen Situationen müssen Arrays manipuliert werden, um ein programmatisches Ziel zu erreichen. Dies könnte das Sortieren, Suchen usw. sein. Wir werden sehen, wie Generics uns beim Suchen helfen können.

Der Hauptgrund, warum wir eine generische Funktion für die Suche verwenden, besteht darin, dass wir ein Array unabhängig von der Art der darin enthaltenen Objekte suchen möchten.

func find  (Array: [T], Element: T) -> Int? var index = 0 while (index < array.count)  if(item == array[index])  return index  index++  return nil; 

Im obigen Beispiel ist das find (array: item :) Funktion akzeptiert ein Array des generischen Typs T und sucht es nach einer Übereinstimmung mit Artikel das ist auch vom Typ T.

Es gibt jedoch ein Problem. Wenn Sie versuchen, das obige Beispiel zu kompilieren, gibt der Compiler einen anderen Fehler aus. Der Compiler sagt uns, dass der binäre Operator == kann nicht auf zwei angewendet werden T Operanden. Der Grund liegt auf der Hand, wenn Sie darüber nachdenken. Wir können nicht garantieren, dass der generische Typ T unterstützt die == Operator. Glücklicherweise hat Swift dies abgedeckt. Schauen Sie sich das aktualisierte Beispiel unten an.

func find  (Array: [T], Element: T) -> Int? var index = 0 while (index < array.count)  if(item == array[index])  return index  index++  return nil; 

Wenn wir angeben, dass der generische Typ dem entsprechen muss Gleichwertig Protokoll, dann gibt uns der Compiler einen Pass. Mit anderen Worten, wir wenden eine Einschränkung auf welche Typen an T darstellen kann. Um eine Einschränkung zu einem generischen Element hinzuzufügen, listen Sie die Protokolle zwischen den spitzen Klammern auf.

Aber was bedeutet es, etwas zu sein? Gleichwertig? Es bedeutet einfach, dass es den Vergleichsoperator unterstützt ==.

Gleichwertig ist nicht das einzige Protokoll, das wir verwenden können. Swift hat andere Protokolle, wie z Hashfähigund Vergleichbar. Wir sahen Vergleichbar früher im binären Baumbeispiel. Wenn ein Typ dem entspricht Vergleichbar Protokoll bedeutet das < und > Operatoren werden unterstützt. Ich hoffe, es ist klar, dass Sie jedes beliebige Protokoll verwenden und als Einschränkung verwenden können.

4. Protokolle definieren

Verwenden wir ein Beispiel für ein Spiel, um Einschränkungen und Protokolle in Aktion zu demonstrieren. In jedem Spiel gibt es eine Reihe von Objekten, die im Laufe der Zeit aktualisiert werden müssen. Dieses Update könnte sich auf die Position, den Zustand usw. des Objekts beziehen. Lassen Sie uns jetzt das Beispiel für den Zustand des Objekts verwenden.

Bei der Implementierung des Spiels haben wir viele verschiedene Objekte mit Gesundheit, die Feinde, Verbündete, Neutrale usw. sein könnten. Sie wären nicht alle die gleiche Klasse, da alle unsere verschiedenen Objekte unterschiedliche Funktionen haben könnten.

Wir möchten eine Funktion namens erstellen prüfen(_:)um den Zustand eines Objekts zu überprüfen und seinen aktuellen Status zu aktualisieren. Abhängig vom Status des Objekts können wir Änderungen an seinem Zustand vornehmen. Wir möchten, dass diese Funktion für alle Objekte unabhängig von ihrem Typ funktioniert. Das bedeutet, dass wir machen müssen prüfen(_:)eine generische Funktion. Auf diese Weise können wir die verschiedenen Objekte durchlaufen und aufrufen prüfen(_:) auf jedem Objekt.

Alle diese Objekte müssen über eine Variable verfügen, die ihren Gesundheitszustand darstellt, und eine Funktion, um ihren Zustand zu ändern am Leben Status. Lassen Sie uns ein Protokoll dafür deklarieren und benennen Gesund.

Protokoll fehlerhaft mutierendes Funktionsaleal (Status: Bool) var health: Int get

Das Protokoll definiert, welche Eigenschaften und Methoden der Typ, der dem Protokoll entspricht, implementiert werden muss. Zum Beispiel erfordert das Protokoll, dass jeder Typ, der dem entspricht Gesund Protokoll implementiert die Mutation setAlive (_ :) Funktion. Das Protokoll erfordert auch eine Eigenschaft mit dem Namen Gesundheit.

Lass uns jetzt die prüfen(_:) Funktion haben wir früher erklärt. Wir geben in der Deklaration mit einer Einschränkung an, dass der Typ T muss dem entsprechen Gesund Protokoll.

Funkcheck(inout Objekt: T) if (object.health <= 0)  object.setAlive(false)  

Wir prüfen das Objekt Gesundheit Eigentum. Wenn es kleiner oder gleich Null ist, rufen wir auf setAlive (_ :) auf dem Objekt, vorbei falsch. weil T ist erforderlich, um die Gesund Protokoll wissen wir, dass die setAlive (_ :) Funktion kann für jedes Objekt aufgerufen werden, das an das übergeben wird prüfen(_:) Funktion.

5. Zugehörige Typen

Wenn Sie weitere Kontrolle über Ihre Protokolle haben möchten, können Sie verknüpfte Typen verwenden. Sehen wir uns das Beispiel für den binären Baum noch einmal an. Wir möchten eine Funktion erstellen, um Operationen in einem Binärbaum durchzuführen. Wir brauchen eine Möglichkeit, um sicherzustellen, dass das Eingabeargument den von uns definierten binären Baum erfüllt. Um dies zu lösen, können wir ein erstellen BinaryTree Protokoll.

Protokoll BinaryTree typealias dataType mutating func insert (Daten: dataType) Funktionsindex (i: Int) -> dataType var data: dataType get 

Dies verwendet einen zugehörigen Typ typealias dataType. Datentyp ist einem generischen ähnlich. T verhält sich von früher ähnlich wie Datentyp. Wir geben an, dass ein Binärbaum die Funktionen implementieren muss einfügen(_:) und Index(_:)einfügen(_:) akzeptiert ein Argument des Typs Datentyp. Index(_:) kehrt zurück Datentyp Objekt. Wir geben auch an, dass der Binärbaum h sein musseine Eigenschaft haben Daten das ist vom typ Datentyp.

Dank unseres zugehörigen Typs wissen wir, dass unser binärer Baum konsistent ist. Wir können davon ausgehen, dass der Typ an übergeben wurde einfügen(_:), gegeben von Index(_:), und von gehalten Daten ist für jeden gleich. Wenn die Typen nicht alle gleich sind, stoßen wir auf Probleme.

6. Wo Klausel

Mit Swift können Sie auch where-Klauseln mit Generics verwenden. Mal sehen, wie das funktioniert. Es gibt zwei Dinge, bei denen Klauseln uns erlauben, mit Generika zu erreichen:

  • Wir können erzwingen, dass zugeordnete Typen oder Variablen innerhalb eines Protokolls vom gleichen Typ sind.
  • Wir können einem zugehörigen Typ ein Protokoll zuweisen.

Um dies in Aktion zu zeigen, implementieren wir eine Funktion zur Bearbeitung binärer Bäume. Ziel ist es, den Maximalwert zwischen zwei Binärbäumen zu ermitteln.

Der Einfachheit halber fügen wir dem eine Funktion hinzu BinaryTree Protokoll aufgerufen in Ordnung(). In Ordnung ist eine der drei gängigen Tiefgangtypen. Es ist eine Reihenfolge der Baumknoten, die rekursiv, linker Teilbaum, aktueller Knoten, rechter Teilbaum reist.

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

Wir erwarten das in Ordnung() Funktion, um ein Array von Objekten des zugehörigen Typs zurückzugeben. Wir implementieren auch die Funktion twoMax (treeOne: treeTwo :)das akzeptiert zwei binäre Bäume.

func twoMax (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]

Unsere Erklärung ist aufgrund der woher Klausel. Die erste Voraussetzung, B.dataType == T.dataType, besagt, dass die zugehörigen Typen der beiden binären Bäume gleich sein sollten. Dies bedeutet, dass ihre Daten Objekte sollten vom selben Typ sein.

Der zweite Satz von Anforderungen, B.dataType: Comparable, T.dataType: Comparable, besagt, dass die zugehörigen Typen von beiden dem entsprechen müssen Vergleichbar Protokoll. Auf diese Weise können wir den maximalen Wert beim Vergleich überprüfen.

Interessanterweise wissen wir aufgrund der Natur eines binären Baums, dass das letzte Element eines in Ordnung wird das maximale Element in diesem Baum sein. Dies liegt daran, dass in einem Binärbaum der ganz rechte Knoten der größte ist. Wir müssen nur diese beiden Elemente betrachten, um den Maximalwert zu bestimmen.

Wir haben drei Fälle:

  1. Wenn Baum 1 den Maximalwert enthält, ist das letzte Element des Inorders am größten und wir geben es im ersten zurück ob Aussage.
  2. Wenn Tree 2 den Maximalwert enthält, ist das letzte Element des Befehls inorder am größten und wir geben es in das zurück sonst Klausel des ersten ob Aussage.
  3. Wenn ihre Maxima gleich sind, geben wir das letzte Element in der Reihenfolge von Tree Two zurück, was für beide immer noch das Maximum ist.

Fazit

In diesem Tutorial konzentrierten wir uns auf Generika in Swift. Wir haben den Wert von Generics kennengelernt und die Verwendung von Generics in Funktionen, Klassen und Strukturen untersucht. Wir haben auch Generika in Protokollen verwendet und die zugehörigen Typen und where-Klauseln untersucht.

Mit einem guten Verständnis von Generika können Sie jetzt vielseitigeren Code erstellen und schwierige Codierungsprobleme besser lösen.