1. Invarianten und warum sie gebraucht werden
Wenn man Code „nach GefUhl“ schreibt, scheint es zu genUgen, die Eingabedaten einmal zu prUfen und danach wird schon alles irgendwie gut. In der Praxis wird „gut“ nicht von selbst: Daten aEndern sich staendig, der Code waechst, und nach einer Woche erinnert man sich schon nicht mehr daran, welche Felder konsistent sein muessen. Eine Invariante ist wie ein Sicherheitsgurt: Solange alles ruhig ist, wirkt er ueberfluessig, aber im Moment des Unfalls wird er ploetzlich zum Hauptdarsteller.
Eine Invariante ist eine Regel zur Korrektheit des Zustands einer Typ-Instanz, die immer wahr sein muss und nicht nur „normalerweise“.
Beispiele fuer Invarianten (ohne Fanatismus, ganz menschlich):
- availableCopies darf nicht negativ sein.
- availableCopies darf nicht groesser als totalCopies sein.
- Der title eines Buches darf keine leere Zeichenkette sein (oder muss normalisiert werden).
- year eines Buches muss in einem vernUnftigen Bereich liegen (zum Beispiel 1450…das aktuelle Jahr).
Wenn man Invarianten nicht einhaelt, entsteht das unangenehmste Phanomen in der Entwicklung: Daten sehen normal aus, bis sie an einer anderen Stelle still und leise die Logik zerstoeren. Und Bugs dieser Art tauchen gern am Freitagabend auf, als haetten sie einen Kalender.
Wenn wir sagen „eine Regel prUfen“, stellt sich ein Anfaenger meist ein einzelnes if irgendwo an einer Stelle vor. Aber eine Invariante ist keine einmalige PrUfung, sondern die Idee: „Ein Typ hat einen Vertrag.“ Und dieser Vertrag muss bei jedem Lebensereignis erhalten bleiben: wenn ein Objekt erzeugt wird, wenn es geaendert wird, wenn es auf dem Bildschirm ausgegeben wird, wenn anderer Code versucht, direkt etwas zuzuweisen.
Stellen Sie sich vor, Ihr struct ist ein kleiner Tresor. Darin liegen Wertgegenstaende (Daten). Invarianten sind nicht ein Schild „Bitte den Tresor nicht aufbrechen“. Invarianten sind das Schloss. Wenn Sie den Tresor offen gelassen haben (alle Eigenschaften oeffentlich und frei aenderbar), dann hilft Ihnen das Schild nicht.
In Swift werden Invarianten normalerweise durch eine Kombination aus drei Dingen erhalten:
- kontrolliertem Erzeugen (ueber init),
- kontrollierter AEnderung (ueber Methoden und Zugriffsbeschraenkungen),
- Minimierung von AEnderungspunkten (ueber private(set), private Eigenschaften, Normalisierung an einer Stelle).
Die Kernaussage dieser Vorlesung: Die Invariante muss dort leben, wo der Typ wirklich geaendert werden kann.
2. Invarianten in init: Beginne das Leben nicht falsch
init ist die „Geburtsstation“ fuer eine Instanz. Und wenn ein Typ falsch geboren wird, muessen Sie danach entweder staendig die Folgen behandeln, oder irgendwann etwas sehr Eigenartiges und Trauriges debuggen. Deshalb ist die erste Verteidigungslinie fuer Invarianten der Initializer.
Interessante Tatsache aus der Welt des Swift-Designs: In offiziellen Sprachdiskussionen wird ausdruecklich betont, dass Initializer einer der Schluesselmechanismen zur Wahrung von Invarianten sind, und wenn man externem Code Umgehungsmoeglichkeiten gibt, brechen Invarianten.
Beispiel: Ein naiver Buchtyp, den man leicht zerstoeren kann
Beginnen wir mit einem schlechten Beispiel, um den Schmerz zu spueren — und es dann nicht mehr so zu machen.
import Foundation
struct Book {
var title: String
var totalCopies: Int
var availableCopies: Int
}
var b = Book(title: "Swift for People", totalCopies: 3, availableCopies: 3)
b.availableCopies = -10
print(b.availableCopies) // -10
Der Code kompiliert. Swift beschwert sich nicht. Aber der Zustand des Buches ist absurd geworden. Das bedeutet: Die Invariante „nicht negativ“ existiert ueberhaupt nicht.
Normalisierung in init
Solange wir init?, throws und andere Wege, eine Erzeugung „schoen abzulehnen“, noch nicht behandelt haben, verwenden wir heute eine einfache Strategie: Wir bringen die Eingabe auf einen sicheren Wert.
import Foundation
struct Book {
let title: String
let totalCopies: Int
private(set) var availableCopies: Int
init(title: String, totalCopies: Int, availableCopies: Int) {
self.title = title.isEmpty ? "Untitled" : title
self.totalCopies = max(0, totalCopies)
self.availableCopies = min(max(0, availableCopies), self.totalCopies)
}
}
Hier haben wir sofort drei wichtige Dinge getan.
- Erstens haben wir title normalisiert: Die leere Zeichenkette wurde durch "Untitled" ersetzt.
- Zweitens haben wir nicht zugelassen, dass totalCopies negativ wird.
- Drittens haben wir availableCopies in den Bereich 0...totalCopies eingeklemmt.
Jetzt kann der Typ nicht in einem inkorrekten Zustand starten.
Warum init die beste erste Verteidigungslinie ist
Der Grund ist einfach: init ist der einzige Punkt, an dem das Objekt fuer die Aussenwelt noch nicht existiert. Nach init kann man das Objekt bereits an Funktionen uebergeben, in Arrays ablegen, ausgeben, vergleichen. Und wenn die Invariante in init nicht erfuellt ist, muessen Sie sie danach ueberall im Kopf behalten (und das werden Sie ehrlich gesagt nicht tun).
3. Invarianten in Methoden: Jede AEnderung bewahrt die Regeln
Selbst wenn ein Typ korrekt geboren wurde, kann man ihn durch AEnderungen zerstoeren. Deshalb ist die zweite Verteidigungslinie Methoden, die den Zustand aEndern. Die Logik ist hier fast juristisch: „Wer aendert, der ist auch verantwortlich.“
Da wir eine Lern-CLI-Anwendung „Bibliothek“ bauen (ein vereinfachtes LibraryCLI), fuegen wir dem Buch Verhalten hinzu: Ausgabe und Rueckgabe eines Buchexemplars.
Buch ausgeben: nicht ins Minus geraten
import Foundation
struct Book {
let title: String
let totalCopies: Int
private(set) var availableCopies: Int
mutating func borrowOne() -> Bool {
guard availableCopies > 0 else { return false }
availableCopies -= 1
return true
}
}
Achten Sie auf den Stil: Wir verwenden guard und einen fruehen Ausstieg. Das macht die Regel offensichtlich: Wenn es keine Buecher gibt, aendern wir nichts.
Mini-Test:
var book = Book(title: "Swift", totalCopies: 1, availableCopies: 1)
print(book.borrowOne()) // true
print(book.borrowOne()) // false
Nach dem ersten Aufruf wird availableCopies zu 0, nach dem zweiten bleibt es 0. Die Invariante „nicht negativ“ wird durch die Methode eingehalten.
Buch zurueckgeben: nicht ueber totalCopies hinausgehen
import Foundation
extension Book {
mutating func returnOne() -> Bool {
guard availableCopies < totalCopies else { return false }
availableCopies += 1
return true
}
}
Hier gilt die zweite Regel: Man darf nicht „mehr zurueckgeben, als da war“.
Prufung:
var book = Book(title: "Swift", totalCopies: 1, availableCopies: 1)
print(book.returnOne()) // false
print(book.borrowOne()) // true
print(book.returnOne()) // true
Warum man Regeln besser in Methoden haelt und nicht ausserhalb
Man koennte doch externen Code schreiben:
if book.availableCopies > 0 {
book.availableCopies -= 1
}
Aber das bedeutet, dass jeder Teil des Programms den Zustand aendern kann und man die Regeln ueberall wiederholen muss. Das erzeugt das klassische Problem: An einer Stelle wurde > 0 geprueft, an einer anderen hat man es vergessen, an der dritten >= verwechselt. Und schon hat man „minus zehn Exemplare“.
Eine Methode hingegen fasst die Regel und die AEnderung an einer Stelle zusammen — und wird zur „offiziellen“ Art, den Zustand zu aendern.
4. private(set) als Werkzeug fuer Invarianten
Wenn Sie private(set) zum ersten Mal sehen, scheint es nur eine Spielerei mit Modifikatoren zu sein. In Wirklichkeit ist es sehr praktisch: Man sagt dem externen Code woertlich: „Lesen ja, schreiben nein.“ Und damit gilt: Alle AEnderungen laufen ueber Ihre Methoden, in denen Sie die Invarianten halten.
Gehen wir zu unserem Buchmodell zurueck und zeigen die Idee an einem einfachen Beispiel.
import Foundation
struct Book {
let title: String
let totalCopies: Int
private(set) var availableCopies: Int
}
var book = Book(title: "Swift", totalCopies: 3, availableCopies: 3)
// book.availableCopies -= 1 // ❌ compilation error
Und das ist wunderbar. Ein Compilerfehler ist die beste Art von Fehler: Er passiert nicht beim Benutzer, weil das Programm gar nicht erst gebaut wird.
Wo welche Regel hingehört
Damit das Gehirn es leichter hat, ist so eine kleine Karte hilfreich:
| Situation | Wo man die Regel am besten haelt | Warum |
|---|---|---|
| Der Typ wird erzeugt | |
Vor dem Verlassen von darf das Objekt nicht inkorrekt existieren |
| Der Typ aendert sich „aus gutem Grund“ | Methoden |
Die Methode ist ein einziger AEnderungspunkt plus ein einziger Regelpunkt |
| Eine Eigenschaft darf sich nur „nach den Regeln“ aendern | + Methoden |
Wir verbieten externes Schreiben und lassen nur kontrollierte AEnderungen zu |
| Ein bequemes „abgeleitetes“ Wert braucht man | computed property (get) | Keine Dubletten speichern, keine Inkonsistenzen riskieren |
5. Ein einziger Wahrheitsort fuer die Invariante
Es gibt eine eigene Falle, in die man sehr leicht tappt: Sie haben die Prufung in init gemacht, dann „zur Sicherheit“ noch einmal eine aehnliche Prufung in einer Methode, dann noch einmal in einer anderen, dann eine computed property hinzugefuegt, die ebenfalls etwas klemmt... Und ploetzlich haben Sie vier Stellen, an denen eine einzige Regel lebt.
Das wirkt robust, ist aber in Wahrheit fast ein garantierter zukuenftiger Bug: Sie aendern die Regel an einer Stelle und vergessen die andere.
Die Swift-Community behandelt Invarianten generell als „Versprechen des Typs“. Und wenn man dieses Versprechen umgehen kann, gilt das als Designproblem. In Diskussionen ueber Einschraenkungen fuer Initializer in anderen Modulen wird ein Beispiel angefuehrt, bei dem die Invariante in init gehalten wird, der externe Code aber einen anderen init hinzufuegen und die Regel brechen kann — genau das nennt man gefaehrlich.
Fuer unser jetziges Niveau reicht es, sich eine praktische Formel zu merken:
Eine Regel — ein Wahrheitsort.
Wenn die Regel die Erzeugung betrifft, ist der Wahrheitsort init.
Wenn die Regel die AEnderung betrifft, ist der Wahrheitsort eine Methode (oder ein privater Helper, den Methoden aufrufen).
Schlechte Orte fuer Invarianten
Man will sehr gern Folgendes machen: „Dann korrigiere ich das Feld einfach immer in didSet.“ Technisch geht das. Aber dadurch wird das Modell zu einem „Flugzeug, das man waehrend des Fluges repariert“: Der Zustand war einen Bruchteil einer Sekunde lang inkorrekt, dann wurde er korrigiert, dann wieder inkorrekt... und man weiss nicht mehr sicher, ob ihn nicht jemand in einem seltsamen Moment gelesen hat.
Deshalb lautet die weiche Regel auf diesem Niveau: Wenn eine Regel kritisch ist, muss sie im API des Typs sichtbar sein. Also in init und in den Methoden, die den Zustand aendern. Beobachter (willSet/didSet) lassen wir fuer kurze Reaktionen (zum Beispiel AEnderungen protokollieren oder ein abgeleitetes Feld synchronisieren), aber nicht als Hauptverschluss fuer den Tresor.
6. Beispiel: „Buch + Bibliothek“ und Modellinvarianten
Jetzt verbinden wir alles zu einem kleinen Stueck „Anwendung“, das sich von Vorlesung zu Vorlesung weiterentwickelt. Wir bauen keine vollstaendige CLI-Architektur (das kommt viel spaeter), aber wir behalten schon das grosse Thema im Blick: Bibliothek, Buecher, Ausgabe und Rueckgabe.
Die Bibliothek speichert ein Array von Buechern und moechte keine „kaputten“ Buecher
import Foundation
struct Library {
private(set) var books: [Book] = []
mutating func add(_ book: Book) {
books.append(book)
}
}
Die Invariante von Library ist vorerst einfach: Das Array books darf von aussen gelesen werden, aber nicht direkt geaendert werden. Hinzufuegen geht nur ueber add, und das ist schon eine brauchbare Kontrolle.
Suche nach einem Buch und Ausgabe: Invariante an der Grenze der Aktion
Wir machen eine Methode, die versucht, ein Buch ueber einen Index auszugeben (Array-Indizes kennen Sie bereits, und Sie wissen schon, dass ein Zugriff ausserhalb der Grenzen schmerzt).
import Foundation
extension Library {
mutating func borrowBook(at index: Int) -> Bool {
guard books.indices.contains(index) else { return false }
return books[index].borrowOne()
}
}
Hier ist das wichtige Detail: Wir pruefen zuerst die Grenzen des Index und rufen dann die „offizielle“ Methode borrowOne() auf dem Buch auf. Die Invarianten sind also logisch verteilt: Die Bibliothek ist fuer den gueltigen Index verantwortlich, das Buch fuer die korrekte AEnderung der verfuegbaren Exemplare.
Prufung:
var lib = Library()
lib.add(Book(title: "Swift", totalCopies: 1, availableCopies: 1))
print(lib.borrowBook(at: 0)) // true
print(lib.borrowBook(at: 0)) // false
print(lib.borrowBook(at: 99)) // false
Blockdiagramm: Wo die Regeln geprueft werden
Damit es besser haengen bleibt, hier die Route, „wohin die Steuerung laeuft“:
flowchart TD
A[Externer Code will eine Aktion] --> B{Erzeugen oder AEndern?}
B -->|Erzeugen| C[init]
B -->|AEndern| D[mutating Methode]
D --> E[Regelpruefung + AEnderung]
C --> F[Normalisierung der Eingabe]
E --> G[Zustand ist korrekt]
F --> G
Das ist die Ruhe eines guten Designs: Sie wissen, wo die Regeln leben, und suchen nicht nach dem, der availableCopies auf -10 reduziert hat.
7. Typische Fehler
Fehler Nr. 1: „Ich prufe einmal bei der Eingabe und danach macht es schon alles von allein.“
So ein Denken entsteht oft nach Aufgaben, bei denen man eine Zahl eingelesen, geprueft, die Antwort ausgegeben hat — und fertig. Aber in einem Datenmodell lebt der Zustand lange und aendert sich oft. Wenn die Invariante nur am Eingang geprueft wird, die Eigenschaften danach aber oeffentlich veraendert werden koennen, wird der Typ sehr schnell „kaputt“, ohne dass es offensichtliche Symptome gibt.
Fehler Nr. 2: oeffentliche var-Eigenschaften fuer alles und jedes.
Wenn alle Felder beschreibbar sind, schalten Sie sich selbst die Moeglichkeit ab, Korrektheit zu garantieren. Jeder Code im Projekt kann dann alles moegliche tun. Meist endet das damit, dass man „Kruecken-Prufungen“ im gesamten Code einbaut. In den meisten Lernmodellen ist es schon an diesem Punkt sinnvoll, sich an private(set) und AEnderungsmethoden zu gewoehnen.
Fehler Nr. 3: Die Regel an drei Stellen prufen und sich ueber „Zuverlaessigkeit“ freuen.
Doppelte Prufungen sind keine Zuverlaessigkeit, sondern eine kuenftige Inkonsistenz. Heute heisst die Regel „0…100“, morgen entscheiden Sie sich fuer „1…100“, aendern es an einer Stelle und vergessen es an einer anderen. Besser ist ein einziger Wahrheitsort: init fuer die Erzeugung und Methoden fuer AEnderungen.
Fehler Nr. 4: Methoden bauen, die „ein bisschen alles auf einmal“ aendern.
Wenn borrowOne() ploetzlich auch noch den title und noch irgendetwas in der Bibliothek aendert, zerstoert das die Vorhersagbarkeit. Invarianten sind leichter zu halten, wenn jede Methode eine klare Verantwortung hat: Bedingung geprueft → genau das geaendert, was geaendert werden soll.
Fehler Nr. 5: Versuchen, Invarianten im Nachhinein zu reparieren.
Der Stil „zuerst zuweisen, dann bei Problemen rueckgaengig machen“ wirkt manchmal bequem (vor allem ueber didSet), aber er erzeugt feine und unangenehme Effekte. Viel klarer ist es, wenn eine inkorrekte AEnderung einfach nicht stattfindet: guard nicht bestanden — Zustand nicht geaendert.
GO TO FULL VERSION