Lektion 1.13 · Stufe 1 – Swift-Fundament

Classes, Werttypen und Referenztypen

ca. 15 Minuten Lesezeit

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:

Swift
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:

Swift
class Counter {
    var count = 0
}

let a = Counter()
let b = a
b.count = 10

print(a.count)   // 10 (!)
print(b.count)   // 10

Plö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)
Beispielestruct, enum, Int, String, Array, Dictionaryclass
Bei Zuweisungwird kopiertwird geteilt
Bildein Blatt Papier, das du fotokopiersteine 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.

Text
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:

Swift
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)   // 10

Drei Dinge fallen auf:

  • init ist der Initializer. Er muss jeder Eigenschaft ohne Standardwert einen Wert geben.
  • self.name = name: Parameter und Eigenschaft heißen gleich. self.name meint die Eigenschaft, name allein 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

Swift
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

Swift
func reset(_ player: Player) {
    player.score = 0     // ändert das Original!
}

let anna = Player(name: "Anna", score: 50)
reset(anna)
print(anna.score)   // 0

Bei 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 @Observable so (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:

Swift
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

Swift
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

Swift
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

Swift
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

Anfänger

Übung 1: Vorhersage

Was gibt dieser Code aus? Überlege zuerst, dann prüf es im Playground.

Swift
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.

Fortgeschritten

Ü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!
Swift
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.

Profi

Ü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 @Observable markieren.
  • 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 kein mutating.
  • let bei einer Klasse heißt nur: Der Verweis bleibt gleich. Das Objekt darf sich ändern.
  • Faustregel: standardmäßig struct, class nur bei geteiltem Zustand oder wenn ein Framework es verlangt.
  • In SwiftUI: Views sind Structs, geteilte Daten stecken oft in Klassen (@Observable).

Spickzettel

Swift
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

Lektion geschafft?

Markiere sie als erledigt, damit du deinen Fortschritt siehst.