İçeriğe geç / Skip to content / Zum Inhalt
Ahmet Balaman LogoAhmet Balaman

Swift struct vs. class: Wert- und Referenztypen erklärt

Ahmet Balaman

7 Min. Lesezeit

SwiftStructClassWerttypReferenztypiOS
Swift struct vs. class: Wert- und Referenztypen erklärt

In Swift sehen struct und class von außen fast gleich aus: Beide haben Eigenschaften, Methoden und Initializer. Der eigentliche Unterschied zeigt sich, wenn Sie eine Variable einer anderen zuweisen. Ein Struct ist ein Werttyp: Bei der Zuweisung wird kopiert. Eine Klasse ist ein Referenztyp: Bei der Zuweisung entsteht ein zweiter Zeiger auf dasselbe Objekt. Wer diesen einen Satz wirklich verstanden hat, löst die meisten Prüfungsfragen und die meisten „Warum hat sich dieser Wert geändert?“-Fehler in echten Projekten.

Alle Beispiele unten können Sie in einen Playground oder eine main.swift einfügen und ausführen.

Werttyp: Ein Struct wird kopiert

struct Point {
    var x: Int
    var y: Int
}

var a = Point(x: 1, y: 2)
var b = a          // Kopie
b.x = 10

print(a.x)  // 1
print(b.x)  // 10

In der Zeile b = a entsteht eine unabhängige Kopie von a. Eine Änderung an b berührt a nicht. Auch Int, String, Array, Dictionary und Bool sind in Swift als Structs definiert und verhalten sich genauso. Deshalb können Sie ein Array an eine Funktion übergeben und sicher sein, dass die Funktion Ihr Array nicht verändert.

Beachten Sie: Für Point haben wir keinen Initializer geschrieben. Structs bekommen den Memberwise Initializer geschenkt.

Referenztyp: Eine Klasse wird geteilt

final class Player {
    var name: String
    var score = 0

    init(name: String) {
        self.name = name
    }
}

let p1 = Player(name: "Ada")
let p2 = p1        // zweite Referenz auf dasselbe Objekt
p2.score = 50

print(p1.score)    // 50
print(p1 === p2)   // true

Hier gibt es keine Kopie. p1 und p2 zeigen auf ein einziges Player-Objekt im Speicher; eine Änderung über die eine Referenz ist über die andere sichtbar. Bei der Klasse mussten wir den Initializer selbst schreiben; der Compiler verlangt ihn, sobald eine Eigenschaft keinen Standardwert hat.

Veränderbarkeit: mutating und let

Ändert eine Struct-Methode eine eigene Eigenschaft, muss sie mit mutating markiert werden:

struct Counter {
    private(set) var value = 0

    mutating func increment() {
        value += 1
    }
}

var counter = Counter()
counter.increment()
print(counter.value)   // 1

let fixedCounter = Counter()
// fixedCounter.increment()
// Fehler: cannot use mutating member on immutable value: 'fixedCounter' is a 'let' constant

Ein mit let deklariertes Struct ist vollständig eingefroren: Keine seiner Eigenschaften lässt sich ändern. Der Wert eines Structs ist die Summe seiner Eigenschaften; eine Eigenschaft zu ändern heißt, den Wert selbst zu ändern.

Bei einer Klasse bedeutet let etwas anderes:

let player = Player(name: "Linus")
player.score = 10          // erlaubt: das Innere des Objekts ändert sich
// player = Player(name: "Grace")
// Fehler: cannot assign to value: 'player' is a 'let' constant

let fixiert hier nur die Referenz: player kann auf kein anderes Objekt zeigen, das Objekt selbst bleibt aber veränderlich. Ein mutating für Klassenmethoden gibt es ebenfalls nicht. Deshalb ist ein Struct die sicherere Wahl, wenn Sie eine Unveränderlichkeitsgarantie wollen.

Identität und Gleichheit: === und ==

Es gibt zwei verschiedene Fragen: „Ist das dasselbe Objekt?“ und „Ist der Inhalt gleich?“

struct Money: Equatable {
    var amount: Int
    var currency: String
}

let price1 = Money(amount: 100, currency: "TRY")
let price2 = Money(amount: 100, currency: "TRY")
print(price1 == price2)    // true

let first = Player(name: "Ada")
let second = Player(name: "Ada")
let alias = first

print(first === second)    // false
print(first === alias)     // true

=== existiert nur für Klasseninstanzen und vergleicht die Identität: Ist es dasselbe Objekt im Speicher? Structs haben keine Identität, nur einen Wert; 100 Lira sind überall 100 Lira. Für == muss der Typ Equatable sein. Bei einem Struct, dessen Eigenschaften alle Equatable sind, erzeugt der Compiler == automatisch; bei einer Klasse schreiben Sie es selbst.

Vererbung gibt es nur bei Klassen

class Vehicle {
    var wheels: Int

    init(wheels: Int) {
        self.wheels = wheels
    }

    func describe() -> String {
        "Fahrzeug mit \(wheels) Rädern"
    }
}

class Car: Vehicle {
    init() {
        super.init(wheels: 4)
    }

    override func describe() -> String {
        "Auto: " + super.describe()
    }
}

let vehicle: Vehicle = Car()
print(vehicle.describe())  // Auto: Fahrzeug mit 4 Rädern

Ein Struct kann nicht von einem anderen Struct erben; der Versuch endet mit inheritance from non-protocol type. Gemeinsames Verhalten definieren Sie bei Structs über ein Protocol:

protocol Describable {
    func describe() -> String
}

struct Bicycle: Describable {
    func describe() -> String { "Fahrrad mit 2 Rädern" }
}

Die generelle Tendenz in Swift lautet: Protocols statt Vererbung. Wer von C# oder Java kommt, braucht etwas Zeit, um diese Gewohnheit umzustellen.

deinit: die Lebensdauer eines Objekts

Klasseninstanzen leben über Referenzzählung (ARC). Verschwindet die letzte starke Referenz auf ein Objekt, wird es freigegeben und deinit läuft:

final class FileSession {
    let name: String

    init(name: String) {
        self.name = name
        print("\(name) geöffnet")
    }

    deinit {
        print("\(name) geschlossen")
    }
}

var session: FileSession? = FileSession(name: "log.txt")  // log.txt geöffnet
var sameSession = session
session = nil          // gibt noch nichts aus, sameSession hält das Objekt
sameSession = nil      // log.txt geschlossen

Structs haben kein deinit, weil sie keine geteilte Lebensdauer haben. Halten sich zwei Klassenobjekte gegenseitig mit starken Referenzen, wird keines je freigegeben; das ist ein Retain Cycle, und er begegnet Ihnen am häufigsten bei Closures. Details stehen im Abschnitt zu [weak self] im Closure-Beitrag.

Warum sind SwiftUI-Views Structs?

In SwiftUI schreiben Sie jedes Stück Oberfläche als struct SomeView: View. Der Grund: Eine View ist kein langlebiges Objekt auf dem Bildschirm, sondern eine leichtgewichtige Beschreibung dessen, wie der Bildschirm gerade aussieht. SwiftUI erzeugt diese Beschreibungen neu, sobald sich Daten ändern, und braucht dafür einen Typ, der billig zu erzeugen ist und keinen heimlich geteilten Zustand hat.

Da ein Struct seine eigenen Eigenschaften innerhalb von body nicht ändern kann, wandern veränderliche Daten über Wrapper wie @State in einen von SwiftUI verwalteten Speicher. Wie das in der Praxis aussieht, sehen Sie im Beitrag Erste App mit SwiftUI entwickeln. Für Daten, die sich mehrere Screens teilen, kommen Klassen ins Spiel; diese Seite behandelt der Beitrag zu @State, @Binding und @Observable.

Ein realistisches Szenario: der Warenkorb

In einer typischen App verwenden Sie beides zusammen: Die Daten sind ein Struct, der Verwalter, der diese Daten teilt, ist eine Klasse.

struct CartItem: Identifiable, Equatable {
    let id: Int
    var name: String
    var quantity: Int
    var unitPrice: Double

    var total: Double { Double(quantity) * unitPrice }
}

final class CartStore {
    private(set) var items: [CartItem] = []

    var total: Double {
        items.reduce(0) { $0 + $1.total }
    }

    func add(_ item: CartItem) {
        if let index = items.firstIndex(where: { $0.id == item.id }) {
            items[index].quantity += item.quantity
        } else {
            items.append(item)
        }
    }
}

let store = CartStore()
let checkoutStore = store            // zwei Screens sehen denselben Warenkorb

store.add(CartItem(id: 1, name: "Stift", quantity: 2, unitPrice: 15))
checkoutStore.add(CartItem(id: 1, name: "Stift", quantity: 1, unitPrice: 15))

print(store.items[0].quantity)   // 3
print(checkoutStore.total)       // 45.0

var snapshot = store.items       // Kopie des Arrays
snapshot[0].quantity = 99
print(store.items[0].quantity)   // 3, der Warenkorb bleibt unberührt

Produktliste und Kasse müssen denselben Warenkorb sehen; Teilen ist hier genau das Gewünschte, also ist CartStore eine Klasse. CartItem dagegen sind schlichte Daten ohne Identität; weil es ein Struct ist, kann ein Experiment an snapshot den Warenkorb nicht beschädigen. Die einzige Tür für Änderungen ist die Methode add.

Wann verwenden – und wann nicht?

Machen Sie struct zum Standard. Wechseln Sie zur Klasse, wenn:

  • mehrere Stellen dieselbe Instanz teilen und alle deren Änderungen sehen sollen (Warenkorb, Session, Einstellungsverwaltung);
  • das Objekt eine Identität und eine Lebensdauer hat: eine offene Datei, eine Netzwerkverbindung, ein Timer. Zum Aufräumen brauchen Sie deinit;
  • Sie Vererbung benötigen oder mit einer Apple-API arbeiten, die eine Klasse erwartet, etwa UIViewController.

Bleiben Sie beim Struct für Modelle (Benutzer, Produkt, Koordinate), Codable-Daten aus einer API, SwiftUI-Views und Daten, die Thread-Grenzen überqueren. Weil Werttypen kopiert werden, sind sie in nebenläufigem Code mit async/await deutlich weniger anfällig für Fehler durch geteilten Zustand.

Frage Struct Class
Was passiert bei Zuweisung? Wird kopiert Referenz wird geteilt
let-Instanz Vollständig unveränderlich Nur die Referenz ist fix
Vererbung Nein (stattdessen Protocols) Ja
=== und deinit Nein Ja
Geschenkter Initializer Memberwise Nur wenn jede Eigenschaft einen Standardwert hat

Häufige Fehler

1. Eine Kopie ändern und erwarten, dass sich das Original ändert

struct Todo {
    var title: String
    var isDone = false
}

var todos = [Todo(title: "Hausaufgabe")]
var firstTodo = todos[0]
firstTodo.isDone = true
print(todos[0].isDone)   // false

Symptom: kein Fehler, aber die Liste aktualisiert sich nie. todos[0] hat eine Kopie geliefert. Lösung: direkt an Ort und Stelle ändern, also todos[0].isDone = true.

2. mutating vergessen

Symptom: cannot assign to property: 'self' is immutable oder left side of mutating operator isn't mutable: 'self' is immutable. Lösung: die Methode als mutating func deklarieren. Tritt derselbe Fehler in einer SwiftUI-View auf, lautet die Lösung @State, nicht mutating.

3. Eine Klasse unbemerkt teilen

Symptom: Eine Änderung auf einem Screen erscheint auf einem anderen, obwohl der Nutzer „Abbrechen“ getippt hat. Sie haben dem Bearbeitungsscreen die Klasseninstanz selbst übergeben, nicht eine Kopie. Lösung: das Modell als Struct anlegen; der Bearbeitungsscreen arbeitet auf einer Kopie, die erst bei „Speichern“ zurückgeschrieben wird.

4. Für eine Klasse keinen Initializer schreiben

Symptom: class 'Player' has no initializers. Den Memberwise Initializer der Structs gibt es bei Klassen nicht. Geben Sie entweder jeder Eigenschaft einen Standardwert oder schreiben Sie ein init.

Was in Prüfungen und Interviews gefragt wird

„Was gibt dieser Code aus?“ prüft fast immer, ob eine Zuweisung kopiert oder referenziert. Schauen Sie zuerst auf den Typ: Bei struct sind die beiden Variablen unabhängig, bei class sind sie dasselbe.

„Ist Array ein Werttyp?“ Ja. Sind die Elemente Klassen, wird das Array kopiert, die Elemente zeigen aber weiter auf dieselben Objekte. Hätten wir das Todo-Beispiel oben mit einer Klasse geschrieben, käme bei todos[0].isDone true heraus.

„Ist das Kopieren eines großen Arrays nicht langsam?“ Die Collections der Standardbibliothek nutzen Copy-on-Write: Die echte Kopie entsteht erst, wenn eine der Kopien verändert wird. Für selbst geschriebene Structs gilt das nicht automatisch, die Arrays darin verhalten sich aber weiterhin so.

„Liegen Structs auf dem Stack und Klassen auf dem Heap?“ Eine verbreitete Vereinfachung; der Compiler kann je nach Situation anders entscheiden. Begründen Sie Ihre Antwort mit dem Kopier- und Teilverhalten, nicht mit dem Speicherort. Zur Vorbereitung auf solche Fragen hilft Ihnen die Seite zur Prüfungsvorbereitung.

Häufig gestellte Fragen

Ist in Swift ein Struct oder eine Klasse schneller?

Eine allgemeine Antwort gibt es nicht; Structs haben keinen Aufwand für Referenzzählung, was bei den meisten kleinen Modellen hilft, doch ständiges Kopieren sehr großer Structs kann ebenfalls teuer sein. Entscheiden Sie danach, ob Sie Teilen brauchen, nicht nach Geschwindigkeit.

Was passiert, wenn ein Struct eine Klassen-Eigenschaft enthält?

Das Struct wird kopiert, die Klassen-Eigenschaft darin zeigt aber weiter auf dasselbe Objekt. Kopien können sich über dieses Objekt also gegenseitig beeinflussen; wenn Sie Wertsemantik wollen, halten Sie auch die inneren Typen als Structs.

Warum lässt sich eine Eigenschaft einer mit let deklarierten Klasseninstanz ändern?

Weil let die Referenz fixiert, nicht den Inhalt des Objekts. Soll sich die Eigenschaft nicht ändern, deklarieren Sie diese Eigenschaft innerhalb der Klasse als let.

Können Structs Protocols erfüllen?

Ja. Structs können nicht erben, aber beliebig viele Protocols erfüllen; Identifiable, Equatable und Codable sind die häufigsten.

Kommentare