Structs, Klassen, Enums
Ein struct ist ein Werttyp: Jede Zuweisung erzeugt eine unabhängige Kopie. Eine class ist ein Referenztyp: Zuweisungen teilen dasselbe Objekt. In Swift ist struct die Standardwahl, class nimmst du, wenn mehrere Stellen dasselbe Objekt mit Identität teilen müssen. Ein enum beschreibt eine feste Menge von Fällen, jeder Fall kann eigene Werte tragen.
Danach kannst du
- erklärst den Unterschied zwischen Wert- und Referenztypen an einem Beispiel
- entscheidest begründet zwischen struct und class
- schreibst mutating-Methoden und kontrollierst Zugriff mit private(set)
- modellierst Zustände mit Enums und zugehörigen Werten
Voraussetzungen
Umgebung
Swift 6.4.0, Sprachmodus 6
plattformunabhängig, geprüft unter Linux x86-64
Auf dieser Seite
Werttyp und Referenztyp
Structs und Klassen sehen auf den ersten Blick fast gleich aus: Beide haben Eigenschaften, Methoden und Initialisierer. Der entscheidende Unterschied zeigt sich erst bei einer Zuweisung.
struct PunktWert {
var x: Int
var y: Int
}
final class PunktReferenz {
var x: Int
var y: Int
init(x: Int, y: Int) {
self.x = x
self.y = y
}
}
// struct: Zuweisung kopiert den Wert
var a = PunktWert(x: 1, y: 1)
var b = a
b.x = 99
print("struct:", a.x, b.x)
// class: Zuweisung kopiert nur die Referenz
let c = PunktReferenz(x: 1, y: 1)
let d = c
d.x = 99
print("class:", c.x, d.x)
print("dasselbe Objekt?", c === d)
Ausgabe
struct: 1 99 class: 99 99 dasselbe Objekt? true
Bei struct hat b = a den Punkt kopiert. Danach sind es zwei unabhängige Werte, und b.x = 99 lässt a unberührt. Bei class hat d = c nur den Verweis kopiert. c und d zeigen auf dasselbe Objekt, wie c === d bestätigt. Eine Änderung über d sieht man deshalb auch über c.
b.x = 99 und d.x = 99: Werttypen sind unabhängig, Referenzen teilen sich ein Objekt.Neben struct sind auch enum, Tupel und alle Collections der Standardbibliothek Werttypen. String, Array und Dictionary verhalten sich also wie der Punkt links. Das hast du in der Lektion zu Collections schon gesehen.
struct oder class?
Apple empfiehlt in Choosing Between Structures and Classes, standardmäßig Structs zu verwenden. Die Begründung ist praktisch: Bei einem Werttyp kann keine andere Stelle im Programm deine Daten unbemerkt ändern. Das macht Code leichter nachvollziehbar und ist bei Nebenläufigkeit ein großer Vorteil.
Eine Klasse nimmst du, wenn
- mehrere Stellen dasselbe Ding mit eigener Identität teilen sollen, etwa eine Verbindung oder einen Cache,
- du Vererbung brauchst, zum Beispiel weil ein Framework eine Basisklasse vorgibt,
- du mit Objective-C-Code zusammenarbeitest.
final class im Beispiel verhindert, dass jemand von der Klasse erbt. Das ist eine gute Voreinstellung, solange du Vererbung nicht ausdrücklich brauchst.
mutating: ein Struct ändert sich
Methoden eines Structs dürfen dessen Eigenschaften normalerweise nicht ändern. Soll eine Methode das tun, markierst du sie mit mutating:
struct Zaehler {
private(set) var stand = 0
mutating func erhoehe(um schritt: Int = 1) {
stand += schritt
}
}
var z = Zaehler()
z.erhoehe()
z.erhoehe(um: 5)
print(z.stand)
Ausgabe
6
private(set) erlaubt allen, den Stand zu lesen, aber nur dem Struct selbst, ihn zu ändern. So bleibt erhoehe der einzige Weg, den Zähler zu verändern. Fehlt mutating, sagt dir der Compiler, was zu tun ist:
struct Zaehler {
var stand = 0
func erhoehe() {
stand += 1
}
}
Compiler-Meldung
mutating-fehlt.swift:5:15: error: left side of mutating operator isn't mutable: 'self' is immutable
2 | var stand = 0
3 |
4 | func erhoehe() {
| `- note: mark method 'mutating' to make 'self' mutable
5 | stand += 1
| `- error: left side of mutating operator isn't mutable: 'self' is immutable
6 | }
7 | }Eine mutating-Methode kannst du nur auf einer Variable aufrufen. Auf einer let-Konstante eines Structs ist jede Änderung verboten, auch über Methoden. Bei einer Klasse ist das anders: let fixiert dort nur den Verweis, nicht das Objekt. Deshalb konnte im ersten Beispiel d.x = 99 trotz let d geschrieben werden.
Enums mit zugehörigen Werten
Ein enum beschreibt eine feste Auswahl. Jeder Fall kann eigene Werte mitbringen, die nur für diesen Fall gelten:
enum Zahlung {
case bar
case karte(letzteZiffern: String)
case gutschein(code: String, wert: Double)
}
func beleg(_ zahlung: Zahlung) -> String {
switch zahlung {
case .bar:
return "Bar bezahlt"
case .karte(let ziffern):
return "Karte •••• \(ziffern)"
case let .gutschein(code, wert):
return "Gutschein \(code) über \(wert) €"
}
}
let zahlungen: [Zahlung] = [.bar, .karte(letzteZiffern: "4821"), .gutschein(code: "SWIFT10", wert: 10)]
for zahlung in zahlungen {
print(beleg(zahlung))
}
// Rohwerte und CaseIterable
enum Wochentag: Int, CaseIterable {
case montag = 1, dienstag, mittwoch, donnerstag, freitag, samstag, sonntag
}
print(Wochentag.mittwoch.rawValue)
print(Wochentag(rawValue: 7) == .sonntag)
print(Wochentag.allCases.count)
Ausgabe
Bar bezahlt Karte •••• 4821 Gutschein SWIFT10 über 10.0 € 3 true 7
Eine Kartenzahlung hat Ziffern, ein Gutschein hat Code und Wert, eine Barzahlung hat nichts davon. Mit Optionals in einem einzigen Struct müsstest du immer prüfen, welche Felder gerade gelten. Das Enum macht ungültige Kombinationen unmöglich: Es gibt keinen Gutschein ohne Code.
Zusammen mit dem vollständigen switch aus der Lektion zum Kontrollfluss ist das eines der stärksten Werkzeuge in Swift. Fügst du einen neuen Zahlungsweg hinzu, meldet der Compiler jede Funktion, die ihn noch nicht behandelt.
Das zweite Enum zeigt Rohwerte: Jeder Fall hat einen festen Int. Wochentag(rawValue: 7) liefert ein Optional, denn nicht jede Zahl ist ein Wochentag. CaseIterable liefert alle Fälle als Collection.
Häufige Fehler
left side of mutating operator isn't mutable: 'self' is immutable. Eine Methode ändert eine Eigenschaft ohne mutating.
cannot use mutating member on immutable value. Du rufst eine mutating-Methode auf einer let-Konstante auf. Mach daraus ein var.
Eine Änderung taucht an einer unerwarteten Stelle auf. Du arbeitest mit einer Klasse und hast eine Referenz geteilt. Prüfe, ob ein Struct nicht besser passt.
Übungen
- Schreibe ein Struct
Warenkorbmit einem privaten Array von Preisen in Cent, einermutating-Methodehinzufuegen(_ cent: Int)und einer berechneten Eigenschaftsumme. Zeige mit einer Kopie, dass zwei Warenkörbe unabhängig sind. - Modelliere den Zustand eines Downloads als Enum mit den Fällen
wartend,laeuft(prozent: Int),fertig(dateiname: String)undfehlgeschlagen(grund: String). Schreibe eine Funktion, die für jeden Zustand einen Statustext liefert.
Lösung zu Übung 1
struct Warenkorb {
private var preise: [Int] = []
mutating func hinzufuegen(_ cent: Int) {
preise.append(cent)
}
var summe: Int { preise.reduce(0, +) }
}
var meiner = Warenkorb()
meiner.hinzufuegen(499)
meiner.hinzufuegen(1_250)
var deiner = meiner // Kopie
deiner.hinzufuegen(99)
print(meiner.summe, deiner.summe)
Ausgabe
1749 1848
Lösung zu Übung 2
enum Download {
case wartend
case laeuft(prozent: Int)
case fertig(dateiname: String)
case fehlgeschlagen(grund: String)
}
func status(_ d: Download) -> String {
switch d {
case .wartend:
return "Wartet auf Start"
case .laeuft(let prozent):
return "Lädt … \(prozent) %"
case .fertig(let name):
return "Fertig: \(name)"
case .fehlgeschlagen(let grund):
return "Fehler: \(grund)"
}
}
let verlauf: [Download] = [.wartend, .laeuft(prozent: 40), .fertig(dateiname: "swift.pkg"), .fehlgeschlagen(grund: "keine Verbindung")]
for zustand in verlauf {
print(status(zustand))
}
Ausgabe
Wartet auf Start Lädt … 40 % Fertig: swift.pkg Fehler: keine Verbindung