Abschlussartikel zur protokollorientierten Programmierung.
In diesem Teil werden wir uns ansehen, wie generische Typvariablen gespeichert und kopiert werden und wie die Versandmethode damit arbeitet.
Nicht freigegebene Version
protocol Drawable { func draw() } func drawACopy(local: Drawable) { local.draw() } let line = Line() drawACopy(line) let point = Point() drawACopy(point)
Sehr einfacher Code. drawACopy
einen Parameter vom Typ Drawable und ruft dessen Zeichenmethode auf - das ist alles.
Generalisierte Version
Schauen wir uns die verallgemeinerte Version des obigen Codes an:
func drawACopy<T: Drawable>(local: T) { local.draw() } ...
Nichts scheint sich geändert zu haben. Wir können die drawACopy
Funktion immer noch als drawACopy
Version bezeichnen und nichts weiter, als die interessanteste wie immer unter der Haube.
Generalisierter Code weist zwei wichtige Merkmale auf:
- statischer Polymorphismus (auch als parametrisch bezeichnet)
- definierter und eindeutiger Typ im Kontext des Aufrufs (generischer Typ T wird zur Kompilierungszeit definiert)
Betrachten Sie dies mit einem Beispiel:
func foo<T: Drawable>(local: T) { bar(local) } func bar<T: Drawable>(local: T) { ... } let point = Point(...) foo(point)
Der interessanteste Teil beginnt mit dem Aufruf der Funktion foo
. Der Compiler kennt den Typ des variablen point
genau - es ist nur Punkt. Außerdem kann der Compiler auf den Typ T: Drawable in der Funktion foo
frei schließen, sobald wir eine Variable des bekannten Point-Typs an diese Funktion übergeben: T = Point. Alle Typen sind zur Kompilierungszeit bekannt und der Compiler kann all seine wunderbaren Optimierungen durchführen - das Wichtigste ist, den foo
Aufruf inline zu setzen.
This: ```swift let point = Point(...) foo<T = Point>(point) Becomes this: ```swift bar<T = Point>(point)
Der Compiler bettet den foo
Aufruf mit seiner Implementierung einfach ein und zeigt auch den generischen Typ des T: Drawable-Balkens an. Mit anderen Worten, der Compiler bettet zuerst einen Aufruf der foo-Methode mit dem Typ T = Point und dann das Ergebnis der vorherigen Einbettung ein - die Balkenmethode mit dem Typ T = Point.
Implementierung generischer Methoden
func drawACopy<T: Drawable>(local: T) { local.draw() } drawACopy(Point(...))
Intern verwendet drawACopy
Swift eine Protokoll-Methodentabelle (die alle Implementierungen der T-Methode enthält) und eine Lebenszyklus-Tabelle (die alle Lebenszyklus-Methoden für die T-Instanz enthält). Im Pseudocode sieht das so aus:
func drawACopy<T: Drawable>(local: T, pwt: T.PWT, vwt: T.VWT) {...} drawACopy(Point(...), Point.pwt, Point.vwt)
VWT und PWT sind assoziierte Typen (AssociatedType) in T - als Typ-Aliase (Typealias) nur besser. Point.pwt und Point.vwt sind statische Eigenschaften.
Da in unserem Beispiel T Punkt ist, ist T gut definiert, daher ist die Erstellung eines Containers nicht erforderlich. In der vorherigen drawACopy
Version von drawACopy
(local: Drawable) wurde die Erstellung eines existenziellen Containers nach Bedarf durchgeführt - wir haben dies im zweiten Teil des Artikels untersucht.
In Funktionen ist aufgrund der Erstellung eines Arguments eine Lebenszyklustabelle erforderlich. Wie wir wissen, werden Argumente in Swift über Werte und nicht über Verknüpfungen übergeben. Sie müssen daher kopiert werden, und die Kopiermethode für dieses Argument gehört wie dieses Argument zur Lebenszyklustabelle. Dort gibt es auch andere Lifecycle-Methoden: Zuweisen, Zerstören und Freigeben.
In generischen Funktionen ist aufgrund der Verwendung von Methoden für generische Codeparameter eine Lebensdauertabelle erforderlich.
Verallgemeinert oder nicht verallgemeinert?
Stimmt es, dass die Verwendung generischer Typen die Codeausführung schneller macht als die Verwendung nur von Protokolltypen? Ist die generalisierte Funktion func foo<T: Drawable>(arg: T)
schneller als ihr protokollartiges Gegenstück fun foo(arg: Drawable)
?
Wir haben festgestellt, dass generischer Code eine statischere Form des Polymorphismus ergibt. Es enthält auch Compiler-Optimierungen, die als "Generic Code Specialization" bezeichnet werden. Mal sehen:
Wieder haben wir den gleichen Code:
func drawACopy<T: Drawable>(local: T) { local.draw() } drawACopy(Point(...)) drawACopt(Line(...))
Durch die Spezialisierung einer generischen Funktion wird eine Kopie mit speziellen generischen Typen dieser Funktion erstellt. Wenn wir beispielsweise drawACopy
mit einer Variablen vom Typ Point aufrufen, drawACopy
der Compiler eine spezielle Version dieser Funktion - drawACopyOfPoint
(local: Point), und wir erhalten:
func drawACopyOfPoint(local: Point) { local.draw() } func drawACopyOfLine(local: Line) { local.draw() } drawACopy(Point(...)) drawACopt(Line(...))
Was kann durch grobe Compileroptimierung davor reduziert werden:
Point(...).draw() Line(...).draw()
Alle diese Tricks sind verfügbar, da generische Funktionen nur aufgerufen werden können, wenn alle generischen Typen definiert sind. In der drawACopy
Methode drawACopy
generische Typ (T) genau definiert.
Generische gespeicherte Eigenschaften
Betrachten Sie ein einfaches Strukturpaar:
struct Pair { let fst: Drawable let snd: Drawable } let pair = Pair(fst: Line(...), snd: Line(...))
Wenn wir dies auf diese Weise verwenden, erhalten wir 2 Zuordnungen auf dem Heap (die genauen Speicherbedingungen in diesem Szenario wurden im zweiten Teil beschrieben), aber wir können dies mit Hilfe eines verallgemeinerten Codes vermeiden.
Die generische Version von Pair sieht folgendermaßen aus:
struct Pair<T: Drawable> { let fst: T let snd: T }
Ab dem Zeitpunkt, an dem der Typ T in der verallgemeinerten Version definiert ist, sind die Eigenschaftstypen fst
und snd
identisch und werden ebenfalls definiert. Da der Typ definiert ist, kann der Compiler diesen beiden Eigenschaften eine spezielle Menge an Speicher fst
- fst
und snd
.
Ausführlicher über die spezialisierte Speichermenge:
Wenn wir mit einer fst
Version von Pair
, können die Eigenschaftstypen fst
und snd
gezeichnet werden. Jeder Typ kann Drawable entsprechen, auch wenn er 10 KB Speicher benötigt. Das heißt, Swift kann keine Schlussfolgerung über die Größe dieses Typs ziehen und verwendet einen universellen Speicherort, z. B. einen existenziellen Container. In diesem Container kann jeder Typ aufbewahrt werden. Im Fall von generischem Code ist der Typ gut erkannt, die tatsächliche Größe der Eigenschaften ist ebenfalls erkennbar, und Swift kann einen speziellen Speicherort erstellen. Zum Beispiel (verallgemeinerte Version):
let pair = Pair(Point(...), Point(...))
Typ T ist jetzt Punkt. Point benötigt N Bytes Speicher und in Pair erhalten wir zwei davon. Swift weist 2 * N Speicher zu und legt dort ein pair
.
Mit der generischen Version von Pair werden unnötige Zuordnungen auf dem Heap vermieden, da Typen leicht erkennbar sind und spezifisch lokalisiert werden können - ohne dass universelle Speichervorlagen erstellt werden müssen, da alles bekannt ist.
Fazit
1. Spezialisierter generischer Code - Werttypen
hat die beste Ausführungsgeschwindigkeit, da:
- Keine Heap-Zuordnung beim Kopieren
- generischer Code - Sie schreiben eine Funktion für einen speziellen Typ
- keine Referenzzählung
- statische Methode Versand
2. Spezialisierter verallgemeinerter Code - Referenztypen
Es hat eine durchschnittliche Ausführungsgeschwindigkeit, da:
- Zuweisungen pro Heap beim Instanziieren
- Es gibt eine Referenzzählung
- dynamische Methodenübermittlung über virtuelle Tabelle
3. Nicht spezialisierter verallgemeinerter Code - kleine Werte
- Keine Heap-Zuordnung - Der Wert wird in den Wertpuffer des existenziellen Containers gestellt
- keine Referenzzählung (da nichts auf den Haufen gelegt wird)
- dynamischer Methodenversand über Protokoll-Methodentabelle
4. Nicht spezialisierter verallgemeinerter Code - große Werte
- Platzierung auf dem Heap - Der Wert wird in den Wertepuffer gestellt
- Es gibt eine Referenzzählung
- dynamischer Versand über Protokoll-Methodentabelle
Dieses Material bedeutet nicht, dass Klassen schlecht sind, Strukturen gut sind und Strukturen in Kombination mit verallgemeinertem Code die besten sind. Wir möchten sagen, dass Sie als Programmierer die Verantwortung haben, ein Werkzeug für Ihre Aufgaben auszuwählen. Klassen sind wirklich gut, wenn Sie große Werte behalten müssen und wenn es eine Semantik von Links gibt. Strukturen eignen sich am besten für kleine Werte und wenn Sie deren Semantik benötigen. Protokolle eignen sich am besten für generischen Code und Strukturen usw. Alle Tools sind spezifisch für die zu lösende Aufgabe und haben positive und negative Seiten.
Und zahlen Sie auch nicht für Dynamik, wenn Sie sie nicht brauchen . Finden Sie die richtige Abstraktion mit den geringsten Laufzeitanforderungen.
- Strukturtypen - Bedeutungssemantik
- Klassenarten - Identität
- verallgemeinerter Code - statischer Polymorphismus
- Protokolltypen - dynamischer Polymorphismus
Verwenden Sie den indirekten Speicher, um mit großen Werten zu arbeiten.
Und vergessen Sie nicht - es liegt in Ihrer Verantwortung, das richtige Werkzeug auszuwählen.
Vielen Dank für Ihre Aufmerksamkeit zu diesem Thema. Wir hoffen, dass diese Artikel Ihnen geholfen haben und interessant waren.
Viel glück