2. März 2020

class vs. struct - Referenztypen und Wertetypen in Swift

Das Typsystem von Swift unterscheidet zwischen Klassen, die mit class deklariert werden, und Wertetypen, die mit struct deklariert werden. Die beiden Arten unterscheiden sich wesentlich in der Handhabung.

Referenztypen mit class definieren

Für eine Klasse wird durch den Aufruf des Initializers Speicherplatz für das Objekt allokiert und eine Referenz auf dieses Objekt zurückgegeben. Wird ein Objekt übergeben, wird lediglich die Referenz auf das Objekt übergeben. Dies macht beispielsweise Sinn für ein Model-Objekt, welches nur einmal erzeugt wird, aber zum Beispiel von verschiedenen UIViewControllern gemeinsam verwendet werden kann:

class TimerModel {
    var startTime: Date?
    var endTime: Date?

    init() {
        self.startTime = Date()
    }
}

Wird ein Objekt einer mit class definierten Klasse übergeben wird immer eine Referenz übergeben:

Referenz bei Referenztyp: Zwei Referenzen auf dasselbe Objekt
var timer1 = TimerModel()
var timer2 = timer1

Bei der Verwendung eines solchen Types muss unbedingt auf diesen Referenz-Charakter geachtet werden. Wird ein Objekt übergeben, muss darauf geachtet werden, dass sich nun potentiell Mehrere diesselbe Instanz des Objektes teilen. Teilweise mag dies erwünscht sein; aber es kann zu Problem führen, wenn einer das Objekt verändert und unvorhergesehenerweise auch das Objekt des anderen verändert. In Sprachen, in denen es lediglich Klassen/Referenztypen gibt, werden daher oft Konventionen eingeführt, wie z.B. dass generell eine Kopie gemacht wird, wenn ein fremdes Objekt übernommen wird oder mit unveränderlichen (immutable) Objekten gearbeitet.

Wertetypen mit struct definieren

In Swift gibt es mit den struct-Typen/Wertetypen eine weitere Möglichkeit für Datentypen, die lediglich Daten kapseln. Im Unterschied zu den Referenztypen entfällt bei solchen Wertetypen die Referenz und die Daten werden unmittelbar abgelegt und bei jeder Übergabe oder Zuweisung kopiert:

Diese Variante würde z.B. Sinn machen, um einen Typ zu definieren, um Geldbeträge abzubilden:

Wertetypen mit struct definieren: Werte sind unmittelbar abgelegt und werden kopiert
struct MoneyAmount {
    var cents: Int
    var currency: String
}

var amount1 = MoneyAmount(cents: 50, currency: "€")
var amount2 = amount1

Hinweis: Die Definition eines Initializers für die Übergabe der initialen Werte für die Eigenschaften kann bei einem struct entfallen, da dieser vom Compiler automatisch angelegt wird.

Veränderliche Wertetypen: mutating

Bei der Definition von Methoden für Wertetypen ist zu beachten, dass nur als mutating deklarierte Methoden Eigenschaften verändern dürfen. Auf diesem Weg stellt der Compiler sicher, dass mit let konstant deklarierte Werte nicht verändert werden.

Oft werden daher für Wertetypen sowohl Methoden angeboten, die den Wert direkt verändern, als auch Methoden, die einen neuen, veränderten Wert zurückliefern. Beispielsweise könnte der MoneyAmount-Typ Methoden für die Rundung anbieten. Den Swift-Namenskonventionen folgend würde round den Wert unmittelbar verändern und rounded einen neuen, gerundeten Wert zurückliefern:

struct MoneyAmount {
    var amount: Decimal
    var currency: String

    mutating func round(digits: Int = 2) {
        var result = self.amount
        NSDecimalRound(&result, &self.amount, digits, .bankers)
        self.amount = result
    }

    func rounded(digits: Int = 2) -> MoneyAmount {
        var newValue = self
        newValue.round(digits: digits)
        return newValue
    }
}

Wann class und wann struct?

Zur Entscheidung für class oder struct ist die Frage maßgeblich: Wird etwas von diesem Typ übergeben, soll eine Referenz auf die Daten übergeben werden oder die Daten selbst?

Wertetypen sind vor allem sinnvoll für Datentypen, bei denen es hauptsächlich darum geht, einige wenige Datenwerte zusammengefasst zu repräsentieren.

Wertetypen können für große Mengen kleiner Objekte (z.B. ein Vector3-Typ für eine Koordinate bei der 3D-Programmierung) sehr sinnvoll sein, da der zusätzliche Overhead der Referenz entfällt.

Wertetypen sind zudem bei der Entwicklung von nebenläufig ausgeführtem Code sehr praktisch, da immer zwangsläufig jeder eine eigene „Kopie“ der Daten bekommt und es daher nie zu Konflikten durch Parallelzugriffe kommen kann.

Für Wertetypen gelten einige Einschränkungen: Sie unterstützen keine Vererbung und können sich nicht selbst beinhalten. Da die enthaltenen Daten bei jeder Übergabe kopiert werden, sind sie eher nicht sinnvoll für Typen mit vielen Eigenschaften.