Protokolle und Generics in Swift
Ein Protokoll beschreibt, was ein Typ können muss, ohne festzulegen, wie. Structs, Klassen und Enums erfüllen es, indem sie die verlangten Eigenschaften und Methoden bereitstellen. Mit Extensions gibst du Protokollen Standardverhalten. Generics schreiben Funktionen und Typen einmal für alle Typen, die bestimmte Protokolle erfüllen, zum Beispiel Comparable.
Danach kannst du
- definierst ein Protokoll und lässt eigene Typen es erfüllen
- gibst Protokollen Standardverhalten mit einer Extension
- schreibst generische Funktionen und Typen mit Einschränkungen
- unterscheidest any Protokoll und generische Parameter
Voraussetzungen
Umgebung
Swift 6.4.0, Sprachmodus 6
plattformunabhängig, geprüft unter Linux x86-64
Auf dieser Seite
Ein Protokoll ist ein Vertrag
Ein Protokoll listet Anforderungen: Eigenschaften mit ihren Typen und Methoden mit ihren Signaturen. Ein Typ, der das Protokoll erfüllt, verspricht, all das bereitzustellen. Code, der mit dem Protokoll arbeitet, muss den konkreten Typ dann nicht mehr kennen.
protocol Form {
var name: String { get }
func flaeche() -> Double
}
// Standardverhalten für alle Formen
extension Form {
func beschreibung() -> String {
"\(name) mit \(flaeche()) Flächeneinheiten"
}
}
struct Rechteck: Form {
let breite: Double
let hoehe: Double
var name: String { "Rechteck" }
func flaeche() -> Double { breite * hoehe }
}
struct Quadrat: Form {
let seite: Double
var name: String { "Quadrat" }
func flaeche() -> Double { seite * seite }
}
let formen: [any Form] = [Rechteck(breite: 3, hoehe: 2), Quadrat(seite: 4)]
for form in formen {
print(form.beschreibung())
}
let gesamt = formen.reduce(0) { $0 + $1.flaeche() }
print("Gesamt:", gesamt)
Ausgabe
Rechteck mit 6.0 Flächeneinheiten Quadrat mit 16.0 Flächeneinheiten Gesamt: 22.0
Rechteck und Quadrat haben nichts gemeinsam außer dem Protokoll Form. Trotzdem stehen beide in einem Array und werden in derselben Schleife behandelt. { get } bei name heißt: Die Eigenschaft muss lesbar sein. Ob sie gespeichert oder wie hier berechnet ist, bleibt dem Typ überlassen.
Die Extension auf Form gibt allen Formen die Methode beschreibung(), ohne dass ein einziger Typ sie selbst schreiben muss. So baut Swift große Teile der Standardbibliothek: Ein Typ erfüllt ein kleines Protokoll und bekommt über Extensions viele Fähigkeiten dazu. Wenn du Sequence erfüllst, bekommst du zum Beispiel map, filter und contains geschenkt.
Erfüllt ein Typ eine Anforderung nicht, meldet der Compiler genau, welche fehlt:
protocol Form {
func flaeche() -> Double
}
struct Kreis: Form {
let radius: Double
}
Compiler-Meldung
nicht-konform.swift:5:8: error: type 'Kreis' does not conform to protocol 'Form'
1 | protocol Form {
2 | func flaeche() -> Double
| `- note: protocol requires function 'flaeche()' with type '() -> Double'
3 | }
4 |
5 | struct Kreis: Form {
| |- error: type 'Kreis' does not conform to protocol 'Form'
| `- note: add stubs for conformance
6 | let radius: Double
7 | }any: der Typ zur Laufzeit
[any Form] ist ein Array, in dem jedes Element irgendein Typ sein darf, der Form erfüllt. Swift verpackt jedes Element dafür in einen Behälter, der sich merkt, welcher Typ darin steckt. Das kostet ein wenig Leistung und hat Grenzen, ist aber genau richtig für gemischte Sammlungen wie diese.
Das Schlüsselwort any macht diese Verpackung sichtbar. Bei einem einfachen Protokoll wie Form akzeptiert der Compiler auch im Sprachmodus 6 noch [Form] ohne any; wir haben es mit Swift 6.4.0 geprüft. Schreib es trotzdem aus: Der Leser sieht so sofort, dass hier ein Behälter im Spiel ist. Bei Protokollen mit zugehörigem Typ (associatedtype) warnt der Compiler bereits und kündigt an, dass es in einem künftigen Sprachmodus ein Fehler wird:
protocol Behaelter {
associatedtype Inhalt
var inhalt: Inhalt { get }
}
func zeigeAlle(_ behaelter: [Behaelter]) {
print(behaelter.count)
}
Compiler-Meldung
ohne-any.swift:6:30: warning: use of protocol 'Behaelter' as a type must be written 'any Behaelter'; this will be an error in a future Swift language mode [#ExistentialAny]
4 | }
5 |
6 | func zeigeAlle(_ behaelter: [Behaelter]) {
| `- warning: use of protocol 'Behaelter' as a type must be written 'any Behaelter'; this will be an error in a future Swift language mode [#ExistentialAny]
7 | print(behaelter.count)
8 | }
[#ExistentialAny]: <https://docs.swift.org/compiler/documentation/diagnostics/existential-any>
ohne-any.swift:6:30: warning: use of protocol 'Behaelter' as a type must be written 'any Behaelter'; this will be an error in a future Swift language mode [#ExistentialAny]
4 | }
5 |
6 | func zeigeAlle(_ behaelter: [Behaelter]) {
| `- warning: use of protocol 'Behaelter' as a type must be written 'any Behaelter'; this will be an error in a future Swift language mode [#ExistentialAny]
7 | print(behaelter.count)
8 | }
[#ExistentialAny]: <https://docs.swift.org/compiler/documentation/diagnostics/existential-any>Die Alternative sind Generics: Wenn eine Funktion nur mit einem bestimmten Typ auf einmal arbeitet, braucht es keinen Behälter.
Generics: einmal für alle Typen
// Eine Funktion für jeden Typ, der vergleichbar ist
func groesstes<T: Comparable>(_ werte: [T]) -> T? {
guard var bisher = werte.first else { return nil }
for wert in werte where wert > bisher {
bisher = wert
}
return bisher
}
print(groesstes([3, 17, 8]) ?? 0)
print(groesstes(["Kiwi", "Apfel", "Zitrone"]) ?? "")
print(groesstes([Double]()) as Any)
// Ein generischer Typ
struct Stapel<Element> {
private var elemente: [Element] = []
mutating func lege(_ e: Element) { elemente.append(e) }
mutating func nimm() -> Element? { elemente.popLast() }
var istLeer: Bool { elemente.isEmpty }
}
var verlauf = Stapel<String>()
verlauf.lege("Start")
verlauf.lege("Lektion 1")
print(verlauf.nimm() ?? "-", verlauf.istLeer)
// Eigene Typen vergleichbar machen
struct Version: Comparable, CustomStringConvertible {
let major: Int
let minor: Int
static func < (a: Version, b: Version) -> Bool {
(a.major, a.minor) < (b.major, b.minor)
}
var description: String { "\(major).\(minor)" }
}
print(groesstes([Version(major: 5, minor: 10), Version(major: 6, minor: 2), Version(major: 6, minor: 0)])!)
Ausgabe
17 Zitrone nil Lektion 1 false 6.2
groesstes<T: Comparable> sagt: Diese Funktion arbeitet mit einem beliebigen Typ T, solange T das Protokoll Comparable erfüllt, sich also mit < und > vergleichen lässt. Beim Aufruf setzt der Compiler den konkreten Typ ein. Für [3, 17, 8] ist T gleich Int, für die Früchte String. Dieselbe Funktion, kein doppelter Code, und trotzdem volle Typprüfung: Ein Array aus Zahlen und Texten gemischt würde nicht kompilieren.
Stapel<Element> ist ein generischer Typ, genau wie Array<Element> aus der Standardbibliothek. Stapel<String>() legt einen Stapel nur für Texte an.
Version zeigt, wie eigene Typen in diese Welt passen: Wer Comparable erfüllt, muss nur < definieren. Danach funktioniert groesstes auch mit Versionen. Der Vergleich nutzt ein Tupel: (a.major, a.minor) < (b.major, b.minor) vergleicht erst die erste Stelle und nur bei Gleichstand die zweite. CustomStringConvertible mit description bestimmt, wie print den Wert ausgibt.
Protokoll, Generic oder keins?
- Du hast mehrere Typen mit gemeinsamem Verhalten, und Code soll mit allen umgehen: Protokoll.
- Eine Funktion oder ein Typ soll mit vielen Typen funktionieren, aber jeweils mit einem auf einmal: Generic mit Protokoll-Einschränkung.
- Du hast genau einen Typ und keine Pläne für einen zweiten: weder noch. Ein Protokoll für einen einzigen Typ macht Code schwerer zu lesen, ohne etwas zu gewinnen.
Die Standardbibliothek liefert viele nützliche Protokolle mit: Equatable (vergleichbar auf Gleichheit), Hashable (für Sets und Dictionary-Schlüssel), Comparable (sortierbar), Codable (in JSON und andere Formate umwandelbar), Sendable (sicher zwischen Tasks weiterzugeben, wichtig für async/await). Für Equatable und Hashable erzeugt der Compiler die Implementierung oft selbst, wenn du das Protokoll nur hinschreibst.
Häufige Fehler
type '…' does not conform to protocol '…'. Eine Anforderung fehlt oder hat eine andere Signatur. Die note zeigt die fehlende Anforderung.
use of protocol '…' as a type must be written 'any …' (Warnung). Schreibe any vor den Protokollnamen oder mach die Funktion generisch.
referencing operator function '>' on 'Comparable' requires that 'T' conform to 'Comparable'. Eine generische Funktion vergleicht Werte, aber T hat keine Einschränkung. Ergänze <T: Comparable>.
Übungen
- Schreibe ein Protokoll
Beschreibbarmit einer Eigenschaftkurztext: String. LassRechteckaus dem ersten Beispiel und ein neues StructBuches erfüllen, und gib beide in einer Schleife aus. - Schreibe eine generische Funktion
zaehle<T: Hashable>(_ werte: [T]) -> [T: Int], die zählt, wie oft jeder Wert vorkommt. Probiere sie mit Zahlen und mit Texten aus.
Lösung zu Übung 1
protocol Beschreibbar {
var kurztext: String { get }
}
struct Rechteck: Beschreibbar {
let breite: Double
let hoehe: Double
var kurztext: String { "Rechteck \(breite) × \(hoehe)" }
}
struct Buch: Beschreibbar {
let titel: String
let seiten: Int
var kurztext: String { "„\(titel)“, \(seiten) Seiten" }
}
let dinge: [any Beschreibbar] = [Rechteck(breite: 3, hoehe: 2), Buch(titel: "Swift lernen", seiten: 240)]
for ding in dinge {
print(ding.kurztext)
}
Ausgabe
Rechteck 3.0 × 2.0 „Swift lernen“, 240 Seiten
Lösung zu Übung 2
func zaehle<T: Hashable>(_ werte: [T]) -> [T: Int] {
var ergebnis: [T: Int] = [:]
for wert in werte {
ergebnis[wert, default: 0] += 1
}
return ergebnis
}
let zahlen = zaehle([3, 1, 3, 3, 2])
print(zahlen.sorted { $0.key < $1.key }.map { "\($0.key): \($0.value)" })
let woerter = zaehle(["ja", "nein", "ja"])
print(woerter.sorted { $0.key < $1.key }.map { "\($0.key): \($0.value)" })
Ausgabe
["1: 1", "2: 1", "3: 3"] ["ja: 2", "nein: 1"]
Weil die Reihenfolge eines Dictionaries nicht festgelegt ist, wird vor der Ausgabe nach Schlüssel sortiert. Das geht, weil Int und String auch Comparable erfüllen.