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

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) // 10In 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) // trueHier 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' constantEin 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' constantlet 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ädernEin 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 geschlossenStructs 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ührtProduktliste 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) // falseSymptom: 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.
Verwandte Artikel
SwiftUI @State, @Binding und @Observable: Datenfluss erklärt
Wann @State, @Binding, @Observable und @Environment in SwiftUI passen, wie sie zu ObservableObject stehen, mit Entscheidungstabelle und typischen Fehlern.
Swift Closures erklärt: $0, @escaping und [weak self]
Die Closure-Syntax in Swift Schritt für Schritt von der Langform bis $0: Trailing Closures, Werte einfangen, @escaping, Retain Cycles und [weak self].
Swift async/await Leitfaden: Task, Actor und @MainActor
Vom Completion Handler zu async/await: Task, async let, TaskGroup, @MainActor, Actors, Abbruch und ein JSON-API-Aufruf mit URLSession in SwiftUI.