7. Februar 2019

Referenz- und Wertetypen in Swift

Das Typsystem von Swift unterscheidet zwischen Referenztypen, die mit class deklariert werden, und Wertetypen, die mit struct deklariert werden.

Referenztypen mit class definieren

Für Referenztypen wird mit dem Aufruf des Initializers Speicherplatz für das Objekt auf dem Heap allokiert und eine Referenz auf dieses Objekt bzw. diesen Speicherbereich zurückgeliefert. Beispielsweise könnte so ein Typ definiert werden, um Geldbeträge abzubilden:

enum Currency {
    case eur
    case usd
}

class MoneyAmount {
    var cents : Int
    var currency : Currency

    init(cents : Int, currency : Currency) {
        self.cents = cents
        self.currency = currency
    }
}

Wird ein Objekt einer mit class definierten Klasse übergeben, wird immer eine Referenz übergeben. Es entstehen dann verschiedene Referenzen auf dasselbe Objekt:

var amount1 = MoneyAmount(cents: 100, currency: .eur)
var amount2 = amount1

Bei der Verwendung eines solchen Types muss unbedingt auf diesen Referenz-Charakter geachtet werden. Wird ein MoneyAmount-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 bieten die Wertetypen eine weitere Möglichkeit für Datentypen, die lediglich einige relativ einfache Datenwerte kapseln. Den obigen Typ könnte man mit struct folgendermaßen definieren:

struct MoneyAmount {
    var cents : Int
    var currency : Currency
}

Der Initializer für die Übergabe der Standard-Werte für die Eigenschaften kann bei einem struct entfallen, da dieser vom Compiler automatisch angelegt wird.

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:

var amount1 = MoneyAmount(cents: 100, currency: .eur)
var amount2 = amount1

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 Prozentrechnung anbieten:

struct MoneyAmount {
    var cents : Int
    var currency : Currency

    func percentValue(percent : Int) -> MoneyAmount {
        // liefert neuen Wert zurück
        return MoneyAmount(cents: (cents*percent)/100, currency : currency)
    }

    mutating func multiplyWith(percent : Int) {
        // Wert selbst wird verändert
        self.cents = (cents*percent)/100
    }
}

Wann class und wann struct?

Wertetypen sind 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 Vector-Typ bei der Spieleprogrammierung) sehr sinnvoll sein, da der zusätzliche Overhead der Referenz entfällt.

Sie 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 (cannot have a stored property that recursively contains it). Da die enthaltenen Daten bei jeder Übergabe kopiert werden, sind sie eher nicht sinnvoll für Typen mit vielen Eigenschaften.

Zur Orientierung helfen folgende Fragen:

Tutorial 1: Datentypen als struct definieren

In den folgenden Kapiteln werden Daten von einem Backend geladen, dass zur Developer API von Quizlet kompatibel sind (Quizlet ist ein Online-Anbieter für Flashcard-Kartenstapel). In diesem Tutorial werden weiter entsprechende Datenstrukturen vorbereitet.

  1. Laden Sie den Start-Stand von dem Flashcards-Beispielprojekt: Flashcards.zip.

  2. Rufen Sie testweise folgende URL im Browser auf:

    Machen Sie sich mit dem Datenformat vertraut. Im folgenden werden bewusst Eigenschaftsnamen gewählt, die kompatibel mit diesem Format sind, um später das Laden dieser Daten zu vereinfachen.

    Tipp: Das Browser-Plug-in JSONView (JSONView für Chrome/Firefox, JSONView für Safari) erleichtert das Betrachten von JSON im Browser:

    Browser-Plug-in JSONView

    Machen Sie sich mit dem Datenformat vertraut. Im Folgenden werden bewusst Eigenschaftsnamen gewählt, die kompatibel mit diesem Format sind, um später das Laden dieser Daten zu vereinfachen.

  3. Deklarieren Sie in FlashcardsAPIClient.swift einen Datentypen SetDownload, der den JSON-Daten entspricht:

    struct SetDownload {
        var id : Int
        var title : String
        var term_count : Int
    }
  4. Passen Sie das Protokoll für das Backend so an, dass dieser Typ verwendet wird und eine Liste mit Beispieldaten zurückgegeben wird:

    protocol FlashcardsAPIClient {
    
        var allSets : [SetDownload] { get }
        func downloadSet(id: Int)
    
    }
    
    
    class DemoFlashcardsAPIClient : FlashcardsAPIClient {
    
        var allSets: [SetDownload] = [
            SetDownload(id: 1, title: "German/English: Common verbs", term_count: 50),
            SetDownload(id: 2, title: "Spanish/English: Animals", term_count: 100)
        ]
        
        // ...
    
    }
  5. Bauen Sie das Projekt und beheben Sie die Compilerfehler in DownloadsTableViewController. Ergänzen Sie die Implementierung so, dass auch die Anzahl der Karten im Kartenstapel im detailTextLabel der Tabellenzellen angezeigt wird:

  6. Übernehmen Sie Ihre Änderungen mit der Commit-Nachricht „14.1. Datentyp für Kartenstapel-Download definiert“.