Lernziel
Nach dieser Lektion kannst du Klassen mit class anlegen und kennst den wichtigsten Unterschied zu Structs: Werttypen werden kopiert, Referenztypen werden geteilt. Du weißt, wann du welches nimmst.
Das Problem: Ein überraschendes Verhalten
Probier diesen Code im Playground aus:
struct Counter {
var count = 0
}
var a = Counter()
var b = a
b.count = 10
print(a.count) // ?
print(b.count) // ?Die Antwort: a.count ist 0, b.count ist 10. Bei var b = a hat Swift eine Kopie angelegt. a und b sind zwei völlig unabhängige Zähler.
Jetzt ändere nur ein einziges Wort, struct wird zu class:
class Counter {
var count = 0
}
let a = Counter()
let b = a
b.count = 10
print(a.count) // 10 (!)
print(b.count) // 10Plötzlich ist auch a.count gleich 10, obwohl du a nie angefasst hast. Und noch seltsamer: Es funktioniert sogar mit let.
Was ist hier los? Genau darum geht es in dieser Lektion. Es ist eines der wichtigsten Konzepte in Swift, und in SwiftUI wirst du es ständig brauchen.
Werttypen und Referenztypen
Swift unterscheidet zwei Arten von Typen:
| Werttyp (Value Type) | Referenztyp (Reference Type) | |
|---|---|---|
| Beispiele | struct, enum, Int, String, Array, Dictionary | class |
| Bei Zuweisung | wird kopiert | wird geteilt |
| Bild | ein Blatt Papier, das du fotokopierst | eine Adresse zu einem Haus |
Das Bild mit dem Papier
Ein Werttyp ist wie ein Blatt Papier. Gibst du jemandem eine Fotokopie, kann er darauf herumkritzeln, wie er will. Dein Original bleibt unverändert.
Das Bild mit der Adresse
Ein Referenztyp ist wie ein Haus. Du gibst niemandem das Haus selbst, sondern einen Zettel mit der Adresse. Gibst du den Zettel weiter, hat der andere eine Kopie des Zettels, aber beide Zettel führen zum selben Haus. Streicht er die Haustür rot an, ist sie auch für dich rot.
Struct: a ──▶ [count: 0] b ──▶ [count: 10] zwei Kopien
Class: a ──┐
├──▶ [count: 10] ein Objekt, zwei Verweise
b ──┘Deshalb funktioniert bei der Klasse auch let: let bedeutet, dass der Zettel nicht ausgetauscht werden darf. a zeigt für immer auf dasselbe Haus. Was im Haus passiert, ist davon nicht betroffen.
Eine Klasse definieren
Klassen sehen Structs sehr ähnlich, mit einem wichtigen Unterschied: Sie haben keinen automatischen Initializer. Du musst ihn selbst schreiben:
class Player {
var name: String
var score: Int
init(name: String, score: Int = 0) {
self.name = name
self.score = score
}
func addPoints(_ points: Int) {
score += points
}
}
let player = Player(name: "Anna")
player.addPoints(10)
print(player.score) // 10Drei Dinge fallen auf:
initist der Initializer. Er muss jeder Eigenschaft ohne Standardwert einen Wert geben.self.name = name: Parameter und Eigenschaft heißen gleich.self.namemeint die Eigenschaft,nameallein den Parameter.- Methoden, die etwas ändern, brauchen kein
mutating. Bei Klassen ändert man ja nicht den Zettel, sondern das Haus.
Wo das wichtig wird
Funktionen und Werttypen
struct Score {
var points = 0
}
func reset(_ score: Score) {
var copy = score
copy.points = 0 // ändert nur die Kopie
}Gibst du eine Struct an eine Funktion, bekommt die Funktion eine Kopie. Sie kann dein Original nicht versehentlich verändern.
Funktionen und Referenztypen
func reset(_ player: Player) {
player.score = 0 // ändert das Original!
}
let anna = Player(name: "Anna", score: 50)
reset(anna)
print(anna.score) // 0Bei einer Klasse kann jede Funktion, die das Objekt bekommt, es verändern. Das ist manchmal genau das, was man will. Es kann aber auch zu Fehlern führen, die schwer zu finden sind: Irgendwo im Code ändert jemand ein Objekt, und an einer ganz anderen Stelle wundert man sich, warum sich die Daten geändert haben.
Gleich oder dasselbe?
Bei Klassen gibt es zwei Arten zu fragen:
===fragt: „Zeigen beide Verweise auf dasselbe Objekt?“==fragt: „Sind die Inhalte gleich?“ Das funktioniert bei Klassen erst, wenn du festlegst, was „gleich“ bedeutet (Lektion 1.15).
Struct oder Class?
Apples Empfehlung ist eindeutig:
Die Faustregel
Nimm standardmäßig struct. Nimm class nur, wenn du einen guten Grund hast.
Gute Gründe für eine Klasse:
- Geteilter Zustand: Mehrere Teile deiner App sollen auf dieselben Daten zugreifen und Änderungen sofort sehen. Ein typisches Beispiel ist ein „Datenspeicher“ für die ganze App.
- Identität: Es geht um ein ganz bestimmtes „Ding“, nicht nur um seine Werte, etwa eine Netzwerkverbindung oder eine geöffnete Datei.
- Ein Framework verlangt eine Klasse. In SwiftUI ist das bei
@Observableso (Lektion 3.1), und auch SwiftData-Modelle sind Klassen (Lektion 3.14).
Für alles andere, also Bücher, Kontakte, Einstellungen, Rechenergebnisse, sind Structs die bessere Wahl. Sie sind einfacher zu verstehen, sicherer und meist auch schneller.
Was das mit SwiftUI zu tun hat
In SwiftUI begegnen dir beide Welten:
- Views sind Structs. SwiftUI erzeugt sie ständig neu und wirft sie weg. Das geht nur, weil Structs so leicht und günstig sind.
- Daten, die mehrere Views teilen, stecken oft in Klassen. Wenn eine Liste und eine Detailansicht dieselben Daten anzeigen und eine Änderung in der einen sofort in der anderen erscheinen soll, brauchst du einen geteilten Datenspeicher.
Genau diese Unterscheidung steckt hinter @State für einfache Werte (Lektion 2.9) und @Observable für Klassen (Lektion 3.1). Ohne das Wissen aus dieser Lektion wären die beiden kaum zu verstehen.
Vererbung: kurz erwähnt
Klassen können von anderen Klassen erben. Eine Unterklasse übernimmt dann alle Eigenschaften und Methoden und kann eigene hinzufügen:
class Animal {
let name: String
init(name: String) { self.name = name }
func sound() -> String { "..." }
}
class Dog: Animal {
override func sound() -> String { "Wuff!" }
}
let dog = Dog(name: "Bello")
print("\(dog.name) sagt \(dog.sound())") // Bello sagt Wuff!override zeigt an, dass eine Methode der Oberklasse ersetzt wird.
Vererbung war früher in der Apple-Welt sehr wichtig. In modernem Swift und SwiftUI verwendet man sie aber nur noch selten. Stattdessen nutzt man Protokolle (Lektion 1.15), die flexibler sind und auch mit Structs funktionieren. Deshalb belassen wir es bei diesem kurzen Blick.
Typische Fehler
Kein Initializer
class Player {
var name: String
}Fehlermeldung
Class 'Player' has no initializers
Klassen brauchen einen init, der allen Eigenschaften ohne Standardwert einen Wert gibt.
self. vergessen im Initializer
init(name: String) {
name = name
}Fehlermeldung
Cannot assign to value: 'name' is a 'let' constant
Swift denkt, du willst dem Parameter name einen Wert zuweisen. Schreib self.name = name.
Unerwartete Änderung
Kein Fehler im Code, aber in der Logik: Du änderst ein Objekt an einer Stelle, und es ändert sich überall. Frag dich: Brauche ich hier wirklich eine Klasse, oder wäre eine Struct sicherer?
mutating in einer Klasse
class Counter {
var count = 0
mutating func increment() { count += 1 }
}Fehlermeldung
'mutating' is not valid on instance methods in classes
In Klassen gibt es kein mutating. Lass es einfach weg.
FAQ
Anfänger
Was bedeutet das überhaupt?Warum gibt es überhaupt zwei Arten? Weil beide für unterschiedliche Aufgaben ideal sind. Werttypen sind sicher und vorhersehbar: Niemand kann dir deine Daten unter den Händen verändern. Referenztypen sind ideal, wenn mehrere Stellen bewusst dieselben Daten teilen sollen.
Sind String und Array auch Structs?
Ja. Deshalb verhalten sie sich wie Werte: var b = a bei einem Array erzeugt eine unabhängige Kopie. Das unterscheidet Swift von vielen anderen Sprachen, in denen Arrays geteilt werden.
Kann ich eine Struct später in eine Klasse umwandeln? Technisch schon, aber Vorsicht: Das Verhalten deines Programms ändert sich dabei grundlegend, wie das Beispiel am Anfang der Lektion zeigt. Entscheide dich deshalb möglichst früh bewusst.
Fortgeschritten
Warum verhält es sich so?Was bedeutet final class?
final verhindert, dass jemand von dieser Klasse erbt. Das macht die Absicht klar und erlaubt dem Compiler einige Optimierungen. Viele Entwickler schreiben standardmäßig final class, außer sie planen ausdrücklich Vererbung.
Was ist ein deinit?
Das Gegenstück zu init: Code, der läuft, kurz bevor ein Objekt aus dem Speicher entfernt wird. Structs haben kein deinit, weil sie keinen eigenen Lebenszyklus haben.
Kann eine Struct eine Klasse als Eigenschaft enthalten? Ja. Dann wird beim Kopieren der Struct aber nur der Verweis auf die Klasse kopiert. Beide Kopien der Struct teilen sich das Objekt. Solche Mischformen sind eine häufige Quelle für Verwirrung, deshalb setze sie bewusst ein.
Profi
Was passiert unter der Haube?Wie weiß Swift, wann ein Objekt gelöscht werden kann?
Über ARC (Automatic Reference Counting). Für jedes Objekt zählt Swift, wie viele Verweise es gibt. Fällt der Zähler auf null, wird das Objekt sofort freigegeben. Zeigen zwei Objekte gegenseitig aufeinander, fällt der Zähler nie auf null, und es entsteht ein Speicherleck. Dagegen helfen weak und unowned.
Sind Kopien von großen Structs nicht teuer?
Die Standardtypen wie Array, String und Dictionary nutzen Copy-on-Write: Sie teilen intern ihren Speicher und kopieren erst, wenn wirklich jemand etwas ändert. Eigene Structs aus wenigen Eigenschaften sind ohnehin klein. In der Praxis ist das Kopieren fast nie ein Problem.
Warum sind Werttypen bei Nebenläufigkeit sicherer?
Weil jeder Thread seine eigene Kopie hat. Zwei Threads können nicht gleichzeitig dasselbe Objekt verändern. Swift 6 nutzt das mit dem Protokoll Sendable: Structs aus Sendable-Bestandteilen sind automatisch sicher zwischen Threads austauschbar, Klassen nur mit zusätzlichem Aufwand (Lektion 3.8).
Übungen
Übung 1: Vorhersage
Was gibt dieser Code aus? Überlege zuerst, dann prüf es im Playground.
struct Point { var x = 0 }
var p1 = Point()
var p2 = p1
p2.x = 5
print(p1.x, p2.x)Und was, wenn Point eine class ist und du let p1 = Point() schreibst?
Lösung Übung 1Erst selbst probieren!
Mit struct: 0 5, denn p2 ist eine Kopie.
Mit class: 5 5, denn beide Verweise zeigen auf dasselbe Objekt.
Übung 2: Geteilter Warenkorb
Baue eine Klasse ShoppingCart mit einem Array items: [String] und einer Methode add(_:). Erzeuge einen Warenkorb und gib ihn an zwei Konstanten phoneView und tabletView. Füge über beide Konstanten je einen Artikel hinzu und gib anschließend die Artikel über phoneView aus. Was passiert, wenn ShoppingCart eine Struct wäre?
Lösung Übung 2Erst selbst probieren!
class ShoppingCart {
var items: [String] = []
func add(_ item: String) {
items.append(item)
}
}
let cart = ShoppingCart()
let phoneView = cart
let tabletView = cart
phoneView.add("Kaffee")
tabletView.add("Milch")
print(phoneView.items) // ["Kaffee", "Milch"]Beide „Ansichten“ teilen denselben Warenkorb, genau das will man hier. Als Struct hätten alle drei Konstanten eine eigene Kopie, und phoneView.add würde nicht einmal kompilieren, weil phoneView mit let angelegt ist.
Übung 3: Entscheidungen
Entscheide für jeden Typ, ob du ihn als struct oder class bauen würdest, und begründe kurz: eine Farbe (RGB), ein Rezept, der zentrale Datenspeicher einer Notizen-App, eine Uhrzeit, ein Download, der im Hintergrund läuft und dessen Fortschritt mehrere Bildschirme anzeigen.
Lösung Übung 3Erst selbst probieren!
- Farbe:
struct. Ein reiner Wert, zwei gleiche Farben sind austauschbar. - Rezept:
struct. Daten, die man anzeigt und bearbeitet. Kopien sind unproblematisch. - Datenspeicher:
class. Mehrere Bildschirme sollen dieselben Notizen sehen und Änderungen sofort mitbekommen. In SwiftUI würdest du ihn als@Observablemarkieren. - Uhrzeit:
struct. Ein Wert. - Download:
class. Er hat eine Identität (genau dieser Download) und einen Zustand, den mehrere Stellen gleichzeitig beobachten.
Zusammenfassung
- Werttypen (
struct,enum,Int,String,Array…) werden bei Zuweisung kopiert. - Referenztypen (
class) werden geteilt: Mehrere Verweise zeigen auf dasselbe Objekt. - Klassen brauchen einen eigenen
init, aber keinmutating. letbei einer Klasse heißt nur: Der Verweis bleibt gleich. Das Objekt darf sich ändern.- Faustregel: standardmäßig
struct,classnur bei geteiltem Zustand oder wenn ein Framework es verlangt. - In SwiftUI: Views sind Structs, geteilte Daten stecken oft in Klassen (
@Observable).
Spickzettel
class Player {
var name: String
init(name: String) { self.name = name }
func rename(_ n: String) { name = n } // kein mutating
}
let a = Player(name: "A")
let b = a // derselbe Spieler!
a === b // true, dasselbe Objekt