Kapitel 9 Der Lebenszyklus von Objekten

Inhalt

In diesem Kapitel geht es um das Leben von Objekten. Sie werden sehen, wie Sie Objekte erzeugen und wieder vernichten. Dabei gehe ich insbesondere auf die Speicherverwaltung von Objekten ein. Hier geht Swift über die Möglichkeiten von Objective-C hinaus und bietet bessere und vor allem sicherere Fähigkeiten. Aus diesem Grund ist dieser Abschnitt auch dann für Sie wichtig, wenn Sie bereits Erfahrung in Objective-C haben.

Zugegebenermaßen ist dies ein etwas trockenes Kapitel, dessen Sinn nicht sofort ersichtlich ist. Selbst wenn es Speicherlecks gibt, wird Ihr Programm meistens dennoch korrekt funktionieren. Allerdings habe ich oft bei komplexeren Projekten erlebt, dass Fehler in der Speicherverwaltung zu Problemen in Programmen führen, die sich im Nachhinein nur schwer finden und beseitigen lassen. Deswegen lohnt es sich, die Zeit zu investieren, um zu verstehen, wie die Speicherverwaltung Ihres Computers funktioniert. Sobald Sie kompliziertere Programme mit vielen tausend Zeilen Programmtext schreiben, macht es wirklich einen Unterschied!

9.1 Objekte erzeugen und vernichten

Sie haben bereits Klassen erzeugt und in Variablen und Konstanten abgelegt. In Abschnitt 7.1.3 haben Sie einen Fall kennengelernt, wo mehrere Variablen und Konstanten auf das gleiche Objekt verwiesen haben. Dafür wurde das Objekt einmal erzeugt und im Freispeicher abgelegt. Anschließend enthalten Variablen und Konstanten die Adresse auf das Objekt, siehe auch Abbildung 7.3. Ich habe Ihnen erklärt, dass ein Objekt automatisch aus dem Speicher geräumt wird, sobald es keine Variablen und Konstanten mehr gibt, die auf das Objekt verweisen. Zumindest habe ich das behauptet. Aber einen konkreten Beweis bin ich Ihnen schuldig geblieben.

Das möchte ich nun ändern und dabei auch zeigen, wie Sie selbst Einfluss auf Klassen vom Anfang bis zum Ende ihres virtuellen Lebens haben. Die Erzeugung von Objekten bezeichnet man als Initialisieren und setzt so genannte Initialisierungsmethoden ein. Diese ermöglichen Ihnen, bei der Erzeugung von Objekten alle notwendigen Einstellungen vorzunehmen.

Das Entfernen von Objekten am Ende ihres virtuellen Lebens bezeichnet man auch als Dealloziieren, auf Englisch »deallocation«.

9.1.1. Erzeugen eines Objekts

Sie wissen bereits, wie Sie ein Objekt erzeugen, nämlich indem Sie den Klassennahmen gefolgt von runden Klammern schreiben, beispielsweise für das Objekt HolzHaufen aus Listing 7.1 in Abschnitt 7.1.1:

// Erzeuge einen HolzHaufen. 
let meinHaufen = HolzHaufen()

Den Aufruf des Klassennamen gefolgt von runden Klammern bezeichnet man auch als Konstruktur. Anschließend dürfen Sie die Eigenschaft hoelzer auf den gewünschten Wert setzen. Nur manchmal ist das ein wenig umständlich  wenn Sie denn mal einen Holzhaufen mit einer anderen Zahl als 18 Hölzern erzeugen wollen, so müssen Sie immer eine zusätzliche Zeile schreiben. Ganz schlimm wird es, wenn Sie eine Klasse mit einem Dutzend Eigenschaften haben  dann sind Sie jedesmal ziemlich beschäftigt, diese zu konfigurieren, nachdem Sie sie erzeugt haben.

Das haben sich die Entwickler von Swift auch gedacht und haben deshalb für Klassen eine Möglichkeit vorgesehen, diese gleich beim Erzeugen zu konfigurieren. Dies geschieht über eine (oder auch mehrere) so genannte init-Methoden. Diese init-Methoden sind ganz spezielle Methoden, die ein Objekt bei der Erzeugung konfigurieren. Ein Objekt darf mehrere solcher Methoden haben. Die init-Methoden unterscheiden sich durch die Zahl und die Art der Parameter. In jedem Fall ist ihre Aufgabe, die Eigenschaften einer Klasse einzurichten, wenn diese erzeugt wird.

Um eine init-Methode hinzuzufügen, lassen Sie das func-Wort weg, nennen die Methode init und geben ihr gegebenenfalls noch Parameter mit auf den Weg. init-Methoden haben keine Rückgabewerte. In Abschnitt 7.2.3 hatten Sie in Listing 7.2 ein Objekt Unternehmer mit drei Eigenschaften kennengelernt: dem kontostand, der erfahrung sowie dem alter. Die Startwerte waren immer 0 für den kontostand und die erfahrung sowie 21 für das alter. Listing 9.1 zeigt das Unternehmer-Objekt mit einer init-Methode, die es erlaubt, alle drei Parameter zu setzen.

import Cocoa 
 
class Unternehmer { 
 
  // Die Eigenschaften des Unternehmers. 
 
  /// Der aktuelle Kontostand. 
  var kontostand:Double 
 
  /// Die gesammelte Erfahrung. 
  var erfahrung:Int 
 
  /// Das aktuelle Alter. 
  var alter:Int 
 
  /// Die einzige init--Methode. 
  init(kontostand:Double, erfahrung:Int, alter:Int) { 
    self.kontostand = kontostand 
    self.erfahrung = erfahrung 
    self.alter = alter 
  } 
}

Listing 9.1 Definition der Klasse »Unternehmer« mit einer »init«-Methode, die alle drei Eigenschaften setzt

Dabei sind folgende Punkte zu beachten:

Sollten Sie allerdings weitere init-Methoden hinzufügen, so achten Sie bitte darauf, dass diese ebenfalls alle Eigenschaften setzen.

var steve = Unternehmer(kontostand: 0.0, erfahrung: 0, alter: 21)

Die init-Methode bewirkt also, dass bei der Erzeugung eines Objektes alle Parameter in die Klammern geschrieben werden. Im Gegensatz zu einem normalen Methodenaufruf gilt dies auch für den Namen des ersten Parameters, in diesem Fall kontostand.

var steve = Unternehmer()

zu schreiben. Wenn Sie es doch versuchen, so erzeugt Xcode eine Fehlermeldung und verweigert die Mitarbeit, bis sie den Fehler korrigieren.

9.1.2. Deinitialisieren eines Objekts

Am Ende eines Objektlebens steht eine andere Methode, die für das Dealloziieren verantwortlich ist. Diese Methode heißt immer deinit und hat wieder eine besondere Schreibweise. Sie lassen das func-Wort weg sowie die Klammern für Parameter. Diese Methode rufen Sie außerdem niemals selbst auf, sondern sie wird automatisch von iOS aufgerufen, sobald ein Objekt aus dem Speicher entfernt wird.

In manchen Programmiersprachen wie C++ spricht man vom Vernichten eines Objektes, bei Objective-C ist eher der Ausdruck Dealloziieren üblich. Bei Swift spricht Apple in der Dokumentation vom Deinitialisieren. Im Grunde genommen ist das Gleiche gemeint: Das Entfernen eines Objekts aus dem Speicher und alle dafür notwendigen Vorbereitungen!

Sie brauchen eine solche Methode in der Praxis relativ selten. Sie kann allerdings recht nützlich sein, um nachzuvollziehen, wann denn tatsächlich ein Objekt aus dem Speicher entfernt wird.

/// Klasse mit Kontrollausgabe für init und deinit. 
class Unternehmer { 
  init () { 
    print("Unternehmer init") 
  }
  deinit { 
    print("Unternehmer deinit") 
  } 

// Erzeuge das Objekt in einem Block. 
print("Vor dem Block") 
if (true) { 
  var steve = Unternehmer() 
  print("Steve wurde erzeugt") 

print("Hinter dem Block")

Listing 9.2 Beispiel der Erzeugung und Dealloziierung eines Objekts »steve« mit Kontrollausgabe

Um dies auszuprobieren, öffnen Sie den Playground »Objekte« und geben Sie Listing 9.2 ein. Hierbei passiert Folgendes:

Stellen Sie sicher, dass Sie die Konsole einsehen bzw. zeigen Sie sie mit IMG + IMG + IMG an. Abbildung 9.1 zeigt die Ausgabe von Listing 9.2.

IMG

Abbildung 9.1 Ausgabe der Erzeugung und Vernichtung des Objekts »steve«

Wie Sie sehen, wird das Objekt tatsächlich innerhalb des Blocks erzeugt. Am Ende des Blocks wird dann die deinit-Methode vom System aufgerufen  das sehen Sie daran, dass die Ausgabe »Unternehmer deinit« genau zwischen den Ausgaben »Steve wurde erzeugt« und »Hinter dem Block« erscheint.


Tipp
Diese Arten von Kontrollausgaben an bestimmten Stellen sind sehr nützlich, um zu verstehen, wie ein Programm abläuft. Insbesondere bei der Suche nach Fehlern kann es sehr nützlich sein.
Ich empfehle Ihnen, mit solchen Kontrollausgaben zu arbeiten, wenn Ihnen nicht klar ist, was passiert. Wie in diesem Beispiel ist es sogar möglich, Methoden zu definieren, deren einziger Zweck in den Kontrollausgaben besteht!
Später werden Sie in Kapitel 19 den Debugger kennenlernen. Der Debugger und Kontrollausgaben sind beides mächtige Werkzeuge, um Fehler zu finden. Sie ersetzen sich nicht, sondern ergänzen sich gegenseitig. Ich empfehle Ihnen, sich mit beiden Methoden zur Fehlersuche vertraut zu machen, damit Sie im Falle eines Falles flexibel vorgehen können.

In Kapitel 19 werden Sie eine weitere Möglichkeit kennenlernen, den Ablauf ihres Programmes zu untersuchen. Dennoch haben Kontrollausgaben weiterhin ihre Berechtigung und jeder Programmierer sollte sie als Werkzeug kennen.

9.2 Objekte als Eigenschaften

Bisher haben Sie es mit einer einzigen Klasse zu tun gehabt und Variablen, die auf Objekte dieser Klasse verweisen. Sobald Sie mehrere Klassen und Objekte haben, wird es besonders interessant: Sie dürfen nämlich ebenfalls Eigenschaften von Klassen definieren, die andere Klassen bezeichnen.

Nehmen wir an, steve und seiner Firma geht es bestens und er beschließt, eine Sekretärin einzustellen, die ihm die langweiligen Arbeiten abnimmt, damit er sich noch besser auf seine Hauptaufgabe konzentrieren kann.

Die Sekretärin wird durch eine weitere Klasse namens Angestellte dargestellt. Zunächst soll diese keine Eigenschaften und Methoden haben  das ist zwar langweilig, aber erlaubt! Der Unternehmer kann nun eine Eigenschaft sekretaerin der Klasse Angestellte besitzen. Diese Situation ist in Listing 9.3 dargestellt. Beachten Sie, dass die sekretaerin entweder direkt oder in einer init-Methode von Unternehmer erzeugt werden muss, sonst erzeugt Xcode einen Fehler! In diesem Fall wird sie direkt erzeugt, durch eine Zuweisung in der Definition der Eigenschaft.

Listing 9.3 enthält außerdem Kontrollausgaben für die beiden Klassen, um anschaulich zu verfolgen, wann was erzeugt und vernichtet wird.

/// Definition der Klasse Angestellte. 
class Angestellte { 
  // Kontrollausgaben, um die Erzeugung und Vernichtung zu verfolgen. 
  init () { 
    print("Angestellte init") 
  }
  deinit { 
    print("Angestellte deinit") 
  }
  // Diese Definition enthält keine wirklich "nützlichen" 
  // Eigenschaften und Methoden. 
}
/// Ein Unternehmer mit Sekretärin. 
class Unternehmer { 
  /// Eine Eigenschaft für die Sekretärin. 
  var sekretaerin:Angestellte = Angestellte()
  // Kontrollausgaben, um die Erzeugung und Vernichtung zu verfolgen. 
  init () { 
    print("Unternehmer init") 
  }
  deinit { 
    print("Unternehmer deinit") 
  } 
}

Listing 9.3 Die Klasse »Angestellte« sowie die Klasse »Unternehmer« mit der Eigenschaft »sekretaerin«


Für Umsteiger
Wenn Sie als Umsteiger die Programmiersprache Objective-C kennen, so wird Ihnen aufgefallen sein, dass eine Variable in Swift immer ein Objekt enthalten muss. Dies war und ist in Objective-C nicht so  dort kann ein Zeiger entweder auf nil zeigen oder auf ein Objekt.
Das war und ist eine »beliebte« Fehlerquelle in Objective-C und Swift hat an dieser Stelle etwas andere Konzepte, um einerseits die Leistungsfähigkeit und Flexibilität der Objektzeiger beizubehalten, andererseits aber Fehlerquellen auszuschließen. Die im folgenden diskutierten Möglichkeiten in Swift unterscheiden sich daher von den aus Objective-C bekannten Zeigertypen und Sie sollten sich als Umsteiger die Zeit nehmen, diese Unterschiede genau zu verstehen.

Dadurch wird automatisch ein Objekt der Klasse Angestellte erzeugt, sobald Sie ein Objekt der Klasse Unternehmer erzeugen:

var steve = Unternehmer()

Davon können Sie sich leicht durch die Kontrollausgaben überzeugen:

Angestellte init 
Unternehmer init

Sowohl Unternehmer als auch Angestellte wurden erzeugt.

IMG

Abbildung 9.2 Ersetzen eines »Unternehmer«-Objektes durch ein neues. Das alte Objekt wird automatisch aus dem Speicher entfernt

Wenn nun das Objekt steve dealloziiert wird, so wird automatisch die dazugehörige sekretaerin ebenfalls aus dem Speicher entfernt. Es sei denn, Sie haben noch eine weitere Variable oder Konstante erzeugt, die steve.sekretaerin enthält. Dies überprüfen Sie, indem Sie schreiben:

print("Neue Zuweisung:") 
steve = Unternehmer()

Dadurch erzeugen Sie einen neuen Unternehmer und weisen ihn steve zu. Der »alte« Unternehmer besitzt nun keine Variablen und/oder Konstanten mehr, die auf ihn verweisen und wird mitsamt seiner Sekretärin aus dem Speicher entfernt. Abbildung 9.2 zeigt die Ausgabe grafisch. Es wird tatsächlich der Unternehmer mitsamt Angestellte-Objekt aus dem Speicher entfernt, sobald ein neuer erzeugt wird  das ist genau der Beweis, den ich Ihnen schuldig geblieben bin!

Grafisch lässt sich diese Situation wie in Abbildung 9.3 gezeigt darstellen, dabei handelt es sich wieder um eine Form eines UML-Diagramms, ein so genanntes Klassendiagramm.

IMG

Abbildung 9.3 UML-Klassendiagramm für die Objekte »Unternehmer« und »Angestellte« aus Listing 9.3

Eine Klassendefinition wird dabei als Kasten dargestellt, in dessen Innerem der Klassenname steht. Um eine Eigenschaft zu symbolisieren, gibt es mehrere Möglichkeiten. Im Folgenden wähle ich eine Linie, an die ich den Namen der Eigenschaft schreibe. Der Kasten, an dem der Name steht, ist dabei die Klasse, die die Eigenschaft enthält.

9.2.1. Optionale Datentypen

In Listing 9.3 muss ein Unternehmer immer eine Sekretärin haben, selbst dann, wenn er sie überhaupt nicht benötigt. Das kommt einer gewissen Bevormundung gleich, denn es könnte ja auch sein, dass Steve irgendwann keine Sekretärin mehr braucht. Und in diesem Fall sollte Swift das tun, was Sie wollen.

Glücklicherweise geht das! In Swift gibt es für diesen Fall eine eigene Art von Datentyp, nämlich die sogenannten optionalen Typen. Jede Variable oder Konstante (und damit auch jede Eigenschaft eines Objektes) kann entweder einen »normalen« oder einen »optionalen« Datentyp enthalten. Die »normalen« Variablen und Konstanten kennen Sie bereits. Die optionalen Datentypen sind neu und bedeuten anschaulich gesprochen, dass sie entweder

Den Fall, dass »nichts« enthalten ist, bezeichnet Swift auch als nil. Ich hatte einen solchen Fall bereits in Abschnitt 7.3 erwähnt, aber noch nicht weiter erläutert. Das möchte ich nun nachholen.

Eine optionale Variable definieren Sie, indem Sie schreiben:

var sekretaerin:Angestellte?

Sie müssen also lediglich ein Fragezeichen an den Datentyp anhängen. Dies bedeutet, dass die Variable sekretaerin entweder ein gültiges Angestellte-Objekt oder nil enthalten darf.

Ersetzen Sie einmal in Listing 9.3 die Eigenschaft sekretaerin vom Typ Angestellte durch einen optionalen Datentyp:

/// Eine Eigenschaft für die Sekretärin. 
var sekretaerin:Angestellte? = Angestellte()

Alles, was Sie dafür tun müssen, ist ein Fragezeichen hinter die Definition der Klasse der Eigenschaft zu schreiben. Nun wird zwar bei der Erzeugung eines Unternehmer-Objektes weiterhin eine Angestellte erzeugt, aber Sie dürfen diese Angestellte ebenfalls auf nil setzen.

Probieren Sie das einmal aus, indem Sie schreiben

// Erzeuge einen neuen Unternehmer. 
var steve = Unternehmer() 
 
// Setze seine Sekretaerin auf nil. 
print("Sekretärin auf nil setzen:") 
steve.sekretaerin = nil

Die Ausgabe von Xcode ist in Abbildung 9.4 zu sehen  in der Tat wird die alte Sekretärin aus dem Speicher entfernt.

IMG

Abbildung 9.4 Ausgabe beim Setzen der »sekretaerin« auf »nil«

Sie können einen optionalen Datentyp in einen nicht-optionalen verwandeln, indem Sie hinter eine Variable ein Ausrufungszeichen ! schreiben. Dadurch wird eine optionale Variable in eine normale Variable umgewandelt, also beispielsweise:

var johanna = steve.sekretaerin!

Auf diesem Wege stellen Sie sicher, dass johanna immer einen Wert hat. Allerdings darf es nicht passieren, dass steve.sekretaerin zu diesem Zeitpunkt nil ist, sonst erzeugt Ihr Programm einen Fehler. Wenn Sie sicherstellen müssen, dass eine optionale Variable wirklich einen Wert hat, können Sie schreiben:

if let johanna = steve.sekretaerin { 
  // johanna ist eine normale Konstante. 
  // Das Angestellte--Objekt wird innerhalb dieses Blocks niemals 
  // aus dem Speicher entfernt. 
} else { 
  // johanna kann nicht benutzt werden, da steve.sekretaerin nil war. 
}

Im ersten if-Block ist johanna eine »normale« Konstante, die nicht nil ist, sondern ein Angestellte-Objekt als Wert hat. Sollte steve.sekretaerin keinen Wert haben, so wird stattdessen der else-Block ausgeführt. Außerdem haben Sie innerhalb des if-Blocks mit johanna eine normale Konstante vom Typ Angestellte. Dadurch können Sie sicher sein, dass johanna innerhalb des Blocks nicht aus dem Speicher entfernt wird.

Seit der Version 2 bietet Swift noch eine weitere Variante, die an eine Stärke von Objective-C angelehnt ist: Sie können einen optionalen Datentyp mit einem angehängten Fragezeichen »?« aufrufen. Sollte der Wert nil sein, so wird der Aufruf nicht ausgeführt und das Ergebnis (sofern Sie es irgendwo weiterbenutzen) ist seinerseits nil. Dies ist insbesondere dann sinnvoll, wenn Sie eine Reihe von Aufrufen machen möchten und eine riesige Kaskade von if-Blöcken vermeiden möchten.

Ein Beispiel ist der Aufruf

let derChef = steve.sekretaerin?.chef

Dieser erlaubt es zu prüfen, wer gerade der Chef von Steves Sekretärin ist. Wobei allerdings sowohl die Sekretärin als auch deren deren chef gleich nil sein dürfen. In einem dieser beiden Fälle ist der Wert von derChef ebenfalls gleich nil, andernfalls ist es ein Objekt vom Typ Unternehmer.

Solch eine Konstruktion wird in der Praxis oft verwendet, wenn es darum geht, bestimmte Anweisungen nur dann auszuführen, wenn eine Reihe von Objekten existiert und andernfalls gar nichts zu tun. In einigen anderen Programmiersprachen erfordert eine solche Konstruktion eine riesige Kaskade von if-Bedingungen.

9.2.2. Starke Typen: Schwache Zeiger

Nun ist unsere Sekretärin vielleicht ein bisschen unzufrieden damit, dass ihr Chef sie zwar genau kennt, sie selbst jedoch ihren Chef nicht. Das Objekt vom Typ Angestellte hat keinerlei Eigenschaft, mit der sie auf den Unternehmer verweist, zu dem sie eigentlich gehört.

Also geben Sie der Klasse Angestellte eine neue Eigenschaft namens chef. Listing 9.4 zeigt die erweiterten Klassen Unternehmer und Angestellte. Die Eigenschaft chef ist eine optionale Eigenschaft vom Typ Unternehmer.

/// Eine Angestellte mit einem Chef. 
class Angestellte { 
  /// Eine Eigenschaft für den Chef. 
  var chef:Unternehmer?
  /// Die init--Methode setzt den Chef nun automatisch. 
  init (chef:Unternehmer?) { 
    self.chef = chef
    print("Angestellte init") 
  }
  deinit { 
    print("Angestellte deinit") 
  } 
}
/// Ein Unternehmer mit Sekretärin. 
class Unternehmer { 
  /// Eine Eigenschaft für die Sekretärin. 
  var sekretaerin:Angestellte?
  // Kontrollausgaben, um die Erzeugung und Vernichtung zu verfolgen. 
  init () { 
    self.sekretaerin = Angestellte(chef:self)
    print("Unternehmer init") 
  }
  deinit { 
    print("Unternehmer deinit") 
  } 
}

Listing 9.4 Erweiterung der Klases »Angestellte« um eine Eigenschaft »chef«

Außerdem besitzen beide Klassen nun init-Methoden, die die Eigenschaften chef und sekretaerin richtig definieren:

Im Ergebnis erzeugt die Zeile

var steve = Unternehmer()

also wieder zwei Objekte, die allerdings »gegenseitig« auf sich verweisen. Abbildung 9.5 zeigt die Situation grafisch als UML-Diagramm.

IMG

Abbildung 9.5 Zwei Objekte »Unternehmer« und »Angestellte« aus Listing 9.4 , die gegenseitig auf sich verweisen

Leider gibt es in diesem Fall ein Problem. Wenn Sie nämlich Folgendes schreiben:

print("Neue Zuweisung:") 
steve = Unternehmer()

so wird wieder ein neuer Unternehmer samt Angestellte erzeugt. Was aber ist mit den alten Objekten? Diese müssten eigentlich aus dem Speicher gelöscht werden. Das passiert aber nicht, siehe Abbildung 9.6!

IMG

Abbildung 9.6 Ausgabe beim Ersetzen eines »Unternehmer«-Objektes aus Listing 9.4 durch ein neues

Woran liegt das? Der Grund ist in Abbildung 9.5 zu finden:

Wenn steve nun nicht mehr auf den alten Unternehmer zeigt, so tut es dennoch der chef seiner Sekretärin! Der Unternehmer und die Angestellte zeigen gegenseitig auf sich und diese beiden Objekte können niemals aus dem Speicher verschwinden!

Dies ist ein Problem, denn ein Programm würde so nach und nach immer mehr Speicher benötigen, je länger es läuft. Und irgendwann würde der Rechner unvermittelt abstürzen, weil der Speicher voll wäre. Diese Art von Fehler nennt sich Speicherleck, auf Englisch Memory leak.

Ein Speicherleck könnten Sie umgehen, indem Sie manuell die Eigenschaften der alten Objekte auf nil setzen, bevor Sie den neuen Unternehmer erzeugen. Das ist allerdings extrem nervig und kann sehr leicht vergessen werden. Andererseits kommen Situationen, in denen zwei Objekte sich gegenseitig kennen müssen, in der Praxis extrem häufig vor. Genau für diesen Fall bietet Swift eine weitere Variante von Variablen an, die so genannten schwachen Referenzen.

Eine schwache Referenz ist eine optionale Referenz, die bei einem Verweis auf Objekte nicht »mitgezählt« wird. Eine schwache Referenz erzeugen Sie, indem Sie vor die Eigenschaft das Wort var das Wort weak schreiben.

Ersetzen Sie einmal in Listing 9.4 die Eigenschaft der Angestellte durch

weak var chef:Unternehmer?

und probieren Sie dann nochmals diese Zeilen aus:

// Erzeuge einen neuen Unternehmer. 
var steve = Unternehmer() 
 
// Ersetze ihn durch ein neues Objekt. 
print("Neue Zuweisung:") 
steve = Unternehmer()

Die Ausgabe ist in Abbildung 9.7 zu sehen. In diesem Fall werden tatsächlich sowohl der Unternehmer als auch die Angestellte aus dem Speicher geräumt, wenn die neuen Objekte erzeugt werden.

IMG

Abbildung 9.7 Zwei Objekte »Unternehmer« und »Angestellte« aus Listing 9.4 , die gegenseitig auf sich verweisen

Was passiert eigentlich genau bei schwachen Referenzen? Wenn diese nicht »mitzählen«, was ist deren Inhalt, wenn »ihr« Objekt gelöscht wird? Die Antwort ist einfach: sie werden automatisch auf nil gesetzt  aus diesem Grund sind schwache Referenzen auch automatisch optionale Datentypen!

// Erzeuge einen neuen Unternehmer. 
var steve = Unternehmer() 
 
// Speichere seine Sekretärin in der Variable johanna. 
var johanna = steve.sekretaerin 
 
// Gib den Inhalt von johanna aus. 
print("Inhalt von johanna: \(johanna)") 
 
// Ersetze steve durch ein neues Objekt. 
steve = Unternehmer() 
 
// Gib den neuen Inhalt von johanna aus. 
print("Inhalt von johanna: \(johanna)")

Listing 9.5 Variablen für Objekte der Klasse »Unternehmer« und »Angestellte«

Das lässt sich auch ganz leicht mit den Zeilen aus Listing 9.5 ausprobieren: In dieser Situation wird die Sekretärin nicht aus dem Speicher entfernt, weil es noch die Variable johanna gibt. Die Ausgabe von print ist in beiden Fällen die gleiche. Wenn Sie im Playground allerdings vor die Definition var johanna das Wort weak schreiben, so ändert sich die Situation grundlegend:

weak var johanna = steve.sekretaerin

Abbildung 9.8 zeigt einen Vergleich der Ausgabe in beiden Fällen. Im zweiten Fall ist die Ausgabe der print-Anweisung anders: johanna ist zunächst Optional(Angestellte) und anschließend nil.

IMG

IMG

Abbildung 9.8 Ausgabe des Playgrounds mit den Anweisungen aus Listing 9.5 , links mit einer normalen und rechts mit einer »schwachen Referenz«

Grafisch sind die beiden Situationen in Abbildung 9.9 dargestellt. Links sehen Sie den Fall, dass johanna eine normale Variable ist, rechts den Fall, dass es eine schwache Referenz ist. Die schwache Referenz wird automatisch auf nil gesetzt, wenn ihr Inhalt aus dem Speicher entfernt wird.

IMG

IMG

Abbildung 9.9 Vergleich normale Variablen und schwache Referenzen. Links ist »johanna« eine normale Variable, rechts eine »schwache Referenz«


Für Umsteiger
Einige Programmiersprachen wie Java und Python bieten einen so genannten Garbage collector an. (Für diesen Begriff ist mir keine gängige deutsche Übersetzung bekannt.) Das ist eine automatische Speicherverwaltung, die ebenfalls die oben genannten Speicherlecks selbstständig erkennnen und auflösen kann.
Sie haben allerdings auch einen gewaltigen Nachteil: Als Programmierer wissen Sie nie so genau, wann das passiert und können sich auch nicht darauf verlassen. Bei Swift ist das Verhalten vollständig reproduzier- und vorhersagbar.
Zugegebenermaßen gibt es Situationen, in denen ein Garbage collector gewisse Vorteile bietet, weil er Ihnen etwas Arbeit spart. Im Allgemeinen ist es aber gerade auf Mobilgeräten wie iPhone oder iPad nicht zweckmäßig, unvorhersagbare Speicherverwaltung und Performanceprobleme zu bekommen.

9.2.3. Objekte ohne Besitzer

In diesem Abschnitt sind Ihnen bisher normale Variablen oder Konstanten, optionale Datentypen sowie  als Spezialfall eines optionalen Datentypen  schwache Referenzen begegnet. Es gibt aber auch noch einen weiteren Fall: ein Objekt, das ohne ein anderes nicht existieren kann! Für diese Situation hat Swift eine weitere Form von Variablen, die so genannten Eigenschaften ohne Besitz.

Sie definieren eine solche Eigenschaft mit Hilfe von unowned var. Ein Beispiel ist eine Klasse Firma, die zwingend einen Gründer braucht, der in diesem Beispiel ein Objekt der Klasse Unternehmer sein soll. Ohne Gründer kann es keine Firma geben. Umgekehrt kann allerdings schon einen Unternehmer geben, der bei einer Firma arbeitet. Er muss nicht zwingend auch ihr Gründer sein.


Hintergrund
Die Bezeichung unowned, also eine Eigenschaft »ohne Besitzer«, ist ein bisschen unglücklich gewählt. Der Grund ist, dass die Programmiersprache Objective-C nur »normale« und »schwache« Variablen erlaubt waren. Darüber hinaus konnten Sie noch die Variante »ignoriere die Speicherverwaltung« wählen, die so genannten unsafe_unretained Zeiger.
Letztere hatten den Nachteil, dass jeder Programmierer selbst dafür verantwortlich war, die Speicherverwaltung korrekt und konsistent zu handhaben. Wenn er einen Fehler dabei gemacht hatte und versehentlich ein Objekt benutzte, das bereits aus dem Speicher geräumt worden war, so ist das Programm manchmal abgestürzt. Aber auch nur manchmal, oft ist es auch korrekt weitergelaufen. Diese Art von Fehlern ist extrem schwer zu finden und wird undefiniertes Verhalten genannt.
Swift benutzt immer noch den etwas unglücklichen Namen, der bedeutet, dass die Speicherverwaltung bei diesem Objekt nicht mitzählt. Aber es bietet die große Verbesserung, dass eine Benutzung eines Objektes, das bereits aus dem Speicher entfernt worden ist, auf jeden Fall zu einem Absturz führt! Das macht es einfacher, diese Art von Speicherfehlern zu finden.

Eine unowned Variable darf nicht optional sein. Denn jeder, der eine solche Variable benutzt, geht zwingend davon aus, dass das Objekt nicht aus dem Speicher entfernt wurde. Listing 9.6 zeigt ein Beispiel anhand der Klassen Firma und Unternehmer. Die erstere erfordert zwingend einen gruender, die zweite hingegen optional eine Firma. Beachten Sie, dass es in diesem Fall kein Speicherleck gibt, da die Firma automatisch aus dem Speicher entfernt wird, wenn ihr Gründer entfernt wird.

Lediglich wenn Sie noch Variablen und Konstanten hätten, die die Firma enthalten, können Sie ihren Gründer nicht aus dem Speicher entfernen, Swift verbietet diese Vorgehensweise grundsätzlich!

/// Ein Unternehmer mit einer (optionalen) Firma. 
class Unternehmer { 
  var unternehmen:Firma? 

 
/// Eine Firma, die zwingend einen Gründer haben muss. 
class Firma { 
  unowned var gruender:Unternehmer 
 
  // Erzeuge die Firma durch einen vorhandenen Gründer. 
  init (gruender: Unternehmer) { 
    // Weise diesem Objekt den Gründer zu. 
    self.gruender = gruender 
 
    // Weise dem Gründer diese Firma zu. 
    self.gruender.unternehmen = self 
  } 
}

Listing 9.6 Klassendefinitionen für »Unternehmer« und »Firma«

Beachten Sie, dass ich in Listing 9.6 eine eigene init-Methode für Firma benutzt habe. Dadurch muss zwingend der Gründer bei der Erzeugung angegeben werden  was ja auch Sinn macht!

Eine Benutzung sieht folgendermaßen aus:

// Erzeuge einen Unternehmer. 
var steve = Unternehmer() 
 
// Dieser gründet eine Firma. 
var apple = Firma(gruender: steve) 
 
// Kontrollausgabe für beide Objekte. 
if steve === apple.gruender { 
  print("Steve ist der Gründer von Apple.") 
}

An dieser Stelle benutzen Sie für den Vergleich drei Gleichheitszeichen ===. Diese Schreibweise vergleicht, ob zwei Objekte identisch sind  also ob beide Objekte wirklich an der gleichen Stelle im Speicher liegen, siehe dazu den fortgeschrittenen Abschnitt 4.4.2. Im Gegensatz zu einem »normalen« Vergleich mittels == schlägt dieser Vergleich fehl, wenn Sie verschiedene Objekte mit identischen Eigenschaften vergleichen.

Die Ausgabe in diesem Fall ist

Steve ist der Gründer von Apple.

Denn die beiden Objekte sind wirklich identisch.

9.2.4. Zusammenfassung

Tabelle 9.1 fasst die verschiedenen Möglichkeiten zusammen, mit denen Sie Variablen im Speicher verwalten.

Deklaration Verhindert Darf nil Verwendung
Dealloziieren werden
var x:Y Ja Nein x darf verändert werden.
Solange x=Y() , wird ein Objekt der
Klasse Y im Speicher gehalten.
let x:Y Ja Nein x darf keine neuen Werte annehmen.
Bei x=Y() wird ein Objekt der
Klasse Y im Speicher gehalten,
solange x gültig ist.
var x:Y? Ja Ja x darf auf nil
gesetzt werden.
Bei x=Y() wird ein Objekt der
Klasse Y im Speicher gehalten.
weak var x:Y Nein Ja x darf auf nil gesetzt werden.
Dies passiert automatisch, wenn das
Objekt in x aus dem Speicher
entfernt wird.
unowned var x:Y Ja Nein Bei x=Y() darf das Objekt der Klasse
Y nicht aus dem Speicher entfernt
werden, solange x gültig ist.

Tabelle 9.1 Verschiedene Arten der Speicherverwaltung für Variablen und Konstanten in Swift

Wenn Sie nicht genau wissen, welche Art von Variablen oder Konstanten Sie verwenden sollen, so habe ich folgende Ratschläge:

9.3 Kopieren von Objekten: Der Weg von Swift

Computer beschäftigen sich sehr häufig mit Kopierfunktionen. Sowohl Textblöcke aus nicht korrekt zitierten Büchern als auch komplette Mediendateien lassen sich mit wenigen Handgriffen duplizieren. Nicht immer sind alle Kopien rechtlich und moralisch einwandfrei, aber grundsätzlich sind sie technisch einfach durchzuführen. In diesem Abschnitt bespreche ich eine weitere Form von Objekten, die so genannten struct-Objekte. Diese ähneln den Klassen-Objekten, die Sie schon kennen, unterscheiden sich aber in einem wichtigen Punkt, nämlich beim Kopieren. Rechtlich brauchen Sie hier glücklicherweise keine Konsequenzen zu befürchten.

Das Kopieren von Datentypen wie Int, Double und Bool kennen Sie bereits sehr gut  jedes Mal, wenn Sie diese neuen Variablen oder Konstanten zuweisen, oder wenn Sie Funktionen aufrufen, werden die Werte kopiert. Bei Objekten ist das nicht mehr ganz so einfach. Wenn Sie diese einer neuen Variablen zuweisen oder als Parameter an eine Funktion übergeben, so wird kein neues Objekt angelegt, sondern es wird ein Zeiger auf das bestehende Objekt kopiert, siehe auch nochmals Abschnitt 7.1.3.

Ist das aber genau das, was immer passieren soll, wenn Sie ein Objekt kopieren? Objekte entsprechen oftmals Dingen, die Sie aus der wirklichen Welt kennen. Ich gebe Ihnen also mal zwei Beispiele: Sie gehen zur Bank und eröffnen ein Konto. Dann zahlen Sie 1000 Euro auf das Konto ein. Intern wird der Computer der Bank wahrscheinlich ein Objekt anlegen, das dem Konto entspricht. Dieses hat dann Eigenschaften unter anderem für die Kontonummer, Ihren Namen und sonstige private Daten, die Sie nur ungern weitergeben möchten. Vor allem aber enthält es auch den Kontostand, in diesem Fall die 1000 Euro.

Nun möchten Sie eine Kreditkarte haben, die von Ihrem Konto Geld abziehen darf. Dafür muss die Kreditkarte also eine Kopie Ihres Kontos bekommen. Aber Ihre Bank wird wohl nicht das komplette Konto samt Kontostand kopieren, sondern lediglich einen Zeiger darauf an Ihre Kreditkarte weitergeben. (Und wenn sie es doch komplett kopiert, teilen Sie mir mal bitte Ihre Bankverbindung mit!)

In diesem Fall ist es also absolut richtig und sinnvoll, nur ein einziges Konto-Objekt zu haben, von dem eine Kopie lediglich einen Verweis erzeugt.

Das zweite Beispiel ist noch handfester: Sie gehen in ein Restaurant und sehen, dass Ihr Tischnachbar gerade ein leckeres Sandwich isst. Sie möchten es auch haben und bestellen eine Kopie dieses Sandwiches. Würde das Sandwich durch ein Objekt im Computer dargestellt, so wäre Ihnen mit einem bloßen Zeiger nicht gedient  denn dieser würde auf das Sandwich auf dem Teller Ihres Nachbarn zeigen und Sie müssten sich das Sandwich mit dem anderen Gast teilen! Stattdessen geht der Kellner in die Küche und erzeugt eine neue Instanz des Sandwiches, indem er (beziehungsweise der Koch) aufgrund des Rezeptes ein neues Sandwich zusammenstellt.

An diesen beiden Beispielen wird deutlich, dass Sie je nach Situation sehr unterschiedliche Vorstellungen davon haben, was beim Kopieren eines Objekts passieren sollte. Wenn Sie ein Objekt entwickeln möchten, das einen Restaurantbesuch abbildet, dann können Sie sogar beide Dinge gleichzeitig haben: Bei jedem Besuch legen Sie eine neue Kopie an. In dieser Kopie möchten Sie einerseits ein Objekt, das das Essen repräsentiert, komplett neu anlegen, andererseits aber lediglich einen Zeiger auf das Konto, mit dem Sie bezahlen, kopieren. Nicht jedoch das Konto selbst.

Diese Fragen treten in der Praxis sehr häufig auf, weswegen es sogar eigene Begriffe gibt, um die beiden Arten von Kopien korrekt zu unterscheiden: Eine komplette Kopie eines Objekts mit seinem gesamten Inhalt nennt sich tiefe Kopie und eine Kopie des Zeigers auf ein Objekt, ohne ein neues Objekt zu erzeugen, nennt sich flache Kopie.

Mit Hilfe der Swift-Klassen lassen sich nun Dinge wie Ihr Bankkonto abbilden. Was aber ist mit dem Sandwich? Genau für diesen Fall bietet Ihnen Swift einen neuen Typ von Objekten an, die so genannten struct-Objekte. Diese werden immer komplett kopiert, wenn Sie sie als Parameter an eine Funktion oder als Wert einer neuen Variablen zuweisen. Sie können immer noch Eigenschaften und Methoden in einer struct verwenden, genauso wie in einer Klasse. Nur das Verhalten bei Zuweisungen ist ein völlig anderes.

Listing 9.7 zeigt eine struct namens Sandwich, die als einzige Eigenschaft name besitzt. Entweder wird der Text »Erdnussbutter und Gelee« als Name verwendet oder es kann bei der Instanziierung ein eigener Name angegeben werden.

/// Ein Sandwich mit einem Namen. 
struct Sandwich { 
  /// Der Name des Sandwich. 
  var name:String 
 
 
  /// Normale Initialisierung mit einem Standardnamen. 
  init() { 
    name = "Erdnussbutter und Gelee" 
  } 
 
  /// Alternative Initialisierung mit eigenem Namen. 
  init(customName:String) { 
    name = customName 
  } 

 
// Anwendungsbeispiele 
var bestellung1 = Sandwich() 
let bestellung2 = Sandwich(customName: "Käse") 
var bestellung3 = bestellung1 
 
// Jetzt: 
// bestellung1.name ist "Erdnussbutter und Gelee". 
// bestellung2.name ist "Käse". 
// bestellung3.name ist "Erdnussbutter und Gelee". 
 
// Ändere den Namen von Bestellung 1. 
bestellung1.name = "Schinken und Käse" 
 
// Jetzt: 
// bestellung1.name ist "Schinken und Käse". 
// bestellung2.name ist "Käse". 
// bestellung3.name ist "Erdnussbutter und Gelee".

Listing 9.7 Verwenden einer »struct« mit Namen »Sandwich«

Beachten Sie, dass zunächst drei verschiedene Objekte der Struktur Sandwich erzeugt werden und in den Variablen bestellung1, bestellung2 und bestellung3 abgespeichert werden. Wenn Sie keine struct, sondern eine class für diesen Fall verwendet hätten, so gäbe es nur zwei verschiedene Objekte und bestellung3 würde das gleiche Objekt wie bestellung1 bezeichnen.

Da bei einer struct aber immer komplette Kopien erzeugt werden, sind bestellung1 und bestellung3 wirklich verschiedene Objekte. Ändern Sie den Namen von bestellung1, so wirkt sich dies nur auf diese, nicht jedoch auf bestellung3 aus.

Dadurch können Sie mit struct alle die Dinge behandeln, von denen es mehrere geben soll, mit class hingegen alle die Dinge, die es nur einmal geben soll.


Hintergrund
Ursprünglich wurden struct-Blöcke in der Programmiersprache C eingeführt, um Daten zu gruppieren. Sie waren gewissermaßen der Vorläufer von Objekten, hatten aber keine Methoden.

9.4 Fortgeschrittenes

Viele Programmiersprachen wie C++ bieten die Möglichkeit, selbst anzugeben, wie sich Objekte verhalten, wenn sie mit Operationen wie + oder == bearbeitet werden. Dies nennt sich Operatorüberladung, auf Englisch operator overloading. Objective-C bietet keine Möglichkeit dazu und deswegen sind manche Dinge dort etwas komplizierter. Mit Swift bietet Apple die Fähigkeit, für Ihre eigenen Objekte Operatoren zu überladen!

9.4.1. Operatorüberladung

Sie haben in Abschnitt 9.2.3 den Vergleich von zwei Objekten mittels === kennengelernt. Dieser prüft, ob zwei Objekte identisch sind, also an der gleichen Adresse im Speicher Ihres Rechners leben. Andererseits konnten Sie zwei String-Objekte mittels == vergleichen. Diese waren gleich, wenn sie die gleichen Zeichen enthalten haben.

Wenn Sie hingegen versucht haben, zwei Unternehmer-Objekte mit == zu vergleichen, so werden Sie eine Fehlermeldung bekommen haben. Der Grund ist, dass der Vergleich == von Haus aus nicht für eigene Objekte definiert ist.

Grundsätzlich können Sie es wie in Listing 9.8 gezeigt definieren: Sie definieren eine Funktion und wählen als Namen den Operator, den Sie überladen möchten und geben dieser die besonderen Parameternamen left und right. Diese Parameter müssen den gewünschten Datentyp haben, den Sie vergleichen wollen  in diesem Fall also zwei Unternehmer-Objekte. Der Rückgabewert ist ein Bool.

/// Vergleiche zwei Unternehmer--Objekte. 
func ==(left: Unternehmer, right: Unternehmer) --> Bool { 
  // Der "normale" Vergleich bzgl. der Speicheradressen. 
  return left === right 
}

Listing 9.8 Überladen des Vergleichsoperators »==« für die Klasse »Unternehmer«

Ab jetzt können Sie Unternehmer-Objekte wie String-Objekte oder wie Zahlen mit == vergleichen, also beispielsweise:

// Erzeuge zwei Objekte, steve und woz. 
let steve = Unternehmer() 
let woz = Unternehmer() 
 
// Vergleiche sie, dies liefert false: 
steve == woz 
 
// Dies liefert true: 
steve == steve

Ähnlich gehen Sie bei anderen Operatoren wie beispielsweise + vor, das Sie für String-Objekte benutzt haben, um diese aneinanderzufügen.

9.5 Aufgaben

1. Ein Raumschiff hat eine Phaserkanone, einen Antrieb mit einer Geschwindigkeit sowie einen Kapitän mit einem Einsatzplan.

Wie würden Sie diese Situation in Swift handhaben, wenn Sie geeignete Klassen und Strukturen sowie deren Eigenschaften schreiben würden?

Hinweis: Es geht hier nur um die Klassen und Strukturen und deren Eigenschaften, die normale, optionale, schwache oder nicht-besitzende Eigenschaften sein dürfen.

2. Schreiben Sie im Beispiel aus Listing 9.7 das Sandwich mit einer class anstelle einer struct und vergewissern Sie sich, dass Sie tatsächlich nur zwei Objekte erzeugt haben und bestellung1 und bestellung3 wirklich identisch sind.