Mit generischem Code können Sie flexible, wiederverwendbare Funktionen und Typen schreiben, die mit jedem Typ arbeiten können, abhängig von den von Ihnen definierten Anforderungen. Sie können Code schreiben, der Doppelarbeit vermeidet und seine Absicht klar und abstrahiert zum Ausdruck bringt. - Schnelle Dokumente
Jeder, der über Swift schrieb, verwendete Generika. Array
, Dictionary
, Set
- die grundlegendsten Optionen für die Verwendung von Generika aus der Standardbibliothek. Wie sind sie im Inneren vertreten? Lassen Sie uns sehen, wie diese grundlegende Funktion der Sprache von Apple-Ingenieuren implementiert wird.
Generische Parameter können entweder durch Protokolle begrenzt oder nicht eingeschränkt werden, obwohl Generika grundsätzlich in Verbindung mit Protokollen verwendet werden, die beschreiben, was genau mit Methodenparametern oder Typfeldern getan werden kann.
Um Generika zu implementieren, verwendet Swift zwei Ansätze:
- Laufzeit - generischer Code ist ein Wrapper (Boxing).
- Compiletime-Way - Generischer Code wird zur Optimierung (Spezialisierung) in einen bestimmten Codetyp konvertiert.
Boxen
Stellen Sie sich eine einfache Methode mit einem unbegrenzten generischen Protokollparameter vor:
func test<T>(value: T) -> T { let copy = value print(copy) return copy }
Der schnelle Compiler erstellt einen einzelnen Codeblock, der aufgerufen wird, um mit einem beliebigen <T>
. Das heißt, unabhängig davon, ob wir test(value: 1)
oder test(value: "Hello")
schreiben, wird derselbe Code aufgerufen und zusätzlich werden Informationen über den Typ <T>
die alle erforderlichen Informationen enthalten, an die Methode übertragen .
Mit solchen unbegrenzten Protokollparametern kann wenig getan werden. Um diese Methode bereits zu implementieren, müssen Sie wissen, wie ein Parameter kopiert wird. Sie müssen seine Größe kennen, um Speicher zur Laufzeit zuzuweisen. Sie müssen wissen, wie Sie ihn zerstören, wenn der Parameter das Feld verlässt Sichtbarkeit. Die Value Witness Table
( VWT
) wird zum Speichern dieser Informationen verwendet. VWT
wird in der Kompilierungsphase für alle Typen erstellt und der Compiler garantiert, dass es zur Laufzeit genau ein solches Objektlayout gibt. Ich möchte Sie daran erinnern, dass Strukturen in Swift als Wert und Klassen als Referenz übergeben werden, sodass für let copy = value
mit T == MyClass
und T == MyStruct
.
Das heißt, das Aufrufen der test
mit dem Übergeben der deklarierten Struktur dort sieht schließlich ungefähr so aus:
Etwas komplizierter wird es, wenn MyStruct
selbst eine generische Struktur ist und die Form MyStruct<T>
. Abhängig von <T>
in MyStruct
unterscheiden sich die Metadaten und MyStruct<Int>
für die Typen MyStruct<Int>
und MyStruct<Bool>
. Dies sind zwei verschiedene Typen zur Laufzeit. Das Erstellen von Metadaten für jede mögliche Kombination von MyStruct
und T
äußerst ineffizient. Swift geht also in die andere Richtung und erstellt in solchen Fällen Metadaten zur Laufzeit unterwegs. Der Compiler erstellt ein Metadatenmuster für die generische Struktur, das mit einem bestimmten Typ kombiniert werden kann und als Ergebnis zur Laufzeit vollständige Typinformationen mit der richtigen VWT
erhält.
Wenn wir Informationen kombinieren, erhalten wir Metadaten, mit denen wir arbeiten können (kopieren, verschieben, zerstören).
Es ist immer noch etwas komplizierter, wenn Protokollbeschränkungen zu Generika hinzugefügt werden. Zum Beispiel beschränken wir <T>
das Equatable
Protokoll. Es sei eine sehr einfache Methode, die die beiden übergebenen Argumente vergleicht. Das Ergebnis ist nur ein Wrapper über die Vergleichsmethode.
func isEquals<T: Equatable>(first: T, second: T) -> Bool { return first == second }
Damit das Programm ordnungsgemäß funktioniert, müssen Sie einen Zeiger auf die Vergleichsmethode static func ==(lhs:T, rhs:T)
. Wie bekomme ich es? Offensichtlich reicht die VWT
Übertragung nicht aus, sie enthält diese Informationen nicht. Um dieses Problem zu lösen, gibt es eine Protocol Witness Table
oder PWT
. Dieses VWT
ähnelt VWT
und wird in der Kompilierungsphase für Protokolle erstellt und beschreibt diese Protokolle.
isEquals(first: 1, second: 2)
- Zwei Argumente bestanden
- Übergeben Sie Metadaten für
Int
damit Sie Objekte kopieren / verschieben / zerstören können - Wir geben die Informationen weiter, die
Int
Equatable
implementiert.
Wenn die Einschränkung die Implementierung eines anderen Protokolls erfordert, z. B. T: Equatable & MyProtocol
, werden Informationen zu MyProtocol
mit dem folgenden Parameter hinzugefügt:
isEquals(..., intIsEquatable: Equatable.witnessTable, intIsMyProtocol: MyProtocol.witnessTable)
Durch die Verwendung von Wrappern zur Implementierung von Generika können Sie alle erforderlichen Funktionen flexibel implementieren, der Overhead kann jedoch optimiert werden.
Generische Spezialisierung
Um die unnötige Notwendigkeit zu beseitigen, während der Programmausführung Informationen zu erhalten, wurde der sogenannte generische Spezialisierungsansatz verwendet. Sie können einen generischen Wrapper durch einen bestimmten Typ durch eine bestimmte Implementierung ersetzen. Zum Beispiel für zwei Aufrufe von isEquals(first: 1, second: 2)
und isEquals(first: "Hello", second: "world")
zusätzlich zur Hauptimplementierung "Wrapper" zwei zusätzliche völlig unterschiedliche Versionen der Methode für Int
und für String
.
Quellcode
Erstellen Sie zunächst eine generic.swift- Datei und schreiben Sie eine kleine generische Funktion, die wir berücksichtigen werden.
func isEquals<T: Equatable>(first: T, second: T) -> Bool { return first == second } isEquals(first: 10, second: 11)
Jetzt müssen Sie verstehen, was daraus schließlich ein Compiler wird.
Dies wird deutlich, wenn Sie unsere .swift- Datei in Swift Intermediate Language oder SIL
kompilieren.
Ein bisschen über SIL und den Kompilierungsprozess
SIL
ist das Ergebnis einer von mehreren Phasen der schnellen Kompilierung.
Der Quellcode .swift wird an Lexer übergeben, der einen abstrakten Syntaxbaum ( AST
) der Sprache erstellt, auf dessen Grundlage die Typprüfung und die semantische Analyse des Codes durchgeführt werden. SilGen konvertiert AST
in SIL
, das als raw SIL
, auf dessen Grundlage Code optimiert und ein optimiertes canonical SIL
erhalten wird, das zur Konvertierung in IR
an IRGen
- ein spezielles Format, das LLVM
versteht und das in .o- , .
konvertiert wird, , .
, .
SIL sehen.
Und wieder zu den Generika
Erstellen Sie eine SIL
Datei aus unserem Quellcode.
swiftc generic.swift -O -emit-sil -o generic-sil.s
Wir erhalten eine neue Datei mit der Erweiterung *.s
. Wenn wir nach innen schauen, sehen wir viel weniger lesbaren Code als das Original, aber immer noch relativ klar.
Suchen Sie die Zeile mit dem Kommentar // isEquals<A>(first:second:)
. Dies ist der Beginn der Beschreibung unserer Methode. Es endet mit einem Kommentar // end sil function '$s4main8isEquals5first6secondSbx_xtSQRzlF'
. Ihr Name kann etwas anders sein. Lassen Sie uns die Methodenbeschreibung etwas analysieren.
%0
und %1
in Zeile 21 sind der first
bzw. second
Parameter- In Zeile 24 erhalten wir Typinformationen und geben sie an
%4
- In Zeile 25 erhalten wir einen Zeiger auf eine Vergleichsmethode aus Typinformationen
- in Zeile 26 Wir rufen die Methode per Zeiger auf und übergeben ihr sowohl Parameter als auch Typinformationen
- In Zeile 27 geben wir das Ergebnis an.
Als Ergebnis sehen wir: Um die notwendigen Aktionen bei der Implementierung der generischen Methode auszuführen, müssen wir während der Programmausführung Informationen aus der Beschreibung des Typs <T>
.
Wir gehen direkt zur Spezialisierung über.
In der kompilierten SIL
Datei folgt unmittelbar nach der Deklaration der allgemeinen isEquals
Methode die Deklaration des Spezialisten für den Typ Int
.
In Zeile 39 wird anstelle des Abrufs der Methode zur Laufzeit aus den "cmp_eq_Int64"
sofort die Methode zum Vergleichen von Ganzzahlen "cmp_eq_Int64"
aufgerufen.
Damit sich die Methode „spezialisieren“ kann, muss die Optimierung aktiviert sein . Das müssen Sie auch wissen
Das Optimierungsprogramm kann nur dann eine Spezialisierung durchführen, wenn die Definition der generischen Deklaration im aktuellen Modul ( Quelle ) sichtbar ist.
Das heißt, die Methode kann nicht auf verschiedene Swift-Module spezialisiert werden (z. B. die generische Methode aus der Cocoapods-Bibliothek). Eine Ausnahme bildet die Standard-Swift-Bibliothek, in der grundlegende Typen wie Array
, Set
und Dictionary
. Alle Generika der Basisbibliothek sind auf bestimmte Typen spezialisiert.
Hinweis: Die Attribute @inlinable
und @usableFromInline
wurden in Swift 4.2 implementiert, @usableFromInline
der Optimierer die Methodenkörper anderer Module @inlinable
und es die Möglichkeit gibt, sie zu spezialisieren. Dieses Verhalten wurde jedoch von mir nicht getestet ( Quelle ).
Referenzen
- Beschreibung der Generika
- Optimierung in Swift
- Detailliertere und ausführlichere Präsentation zum Thema.
- Englischer Artikel