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!
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«.
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:
init
-Methoden alle diese Parameter setzen. Es gibt in Listing 9.1 nur eine einzige init
-Methode und diese setzt in der Tat alle drei Eigenschaften dies ist also kein Problem.Sollten Sie allerdings weitere init
-Methoden hinzufügen, so achten Sie bitte darauf, dass diese ebenfalls alle Eigenschaften setzen.
init
-Methode übergeben. Hierbei ist die Konvention etwas anders als bei einem »normalen« Methodenaufruf Sie müssen nämlich immer alle Parameter benennen! Dies führt dazu, dass ein Unternehmer nur folgendermaßen erzeugt werden darf:
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.
NSObject
habe ich in diesem Listing weggelassen. Dies macht für die folgenden Beispiele keinen Unterschied.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:
init
-Methode besitzt eine Kontrollausgabe, um zu zeigen, wann sie aufgerufen wird.deinit
-Methode besitzt ebenfalls eine solche Kontrollausgabe.if
-Anweisung ein Block erzeugt der Sinn ist hier zu demonstrieren, wie ein Objekt erzeugt und anschließend wieder vernichtet wird, denn die Variable steve
ist nur innerhalb des Blocks gültig und das Objekt wird vernichtet, sobald steve
nicht mehr gültig ist.Stellen Sie sicher, dass Sie die Konsole einsehen bzw. zeigen Sie sie mit +
+
an. Abbildung 9.1 zeigt die Ausgabe von Listing 9.2.
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.
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.
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«
nil
zeigen oder auf ein Objekt.
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.
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.
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.
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.
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.
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:
init
-Methode von Angestellte
hat nun einen Parameter. Dieser Parameter ist vom optionalen Typ Unternehmer?
und sorgt dafür, dass ein Objekt immer mit einem passenden Unternehmer
-Objekt oder mit nil
erzeugt werden muss. Dies ist dann der Anfangswert der Eigenschaft chef
.init
-Methode von Unternehmer
erzeugt nun ein neues Objekt der Klasse Angestellte
und weist als chef
sich selbst zu dazu dient
self
.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.
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!
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:
steve
zeigt zunächst auf das erste Unternehmer
-Objekt.sekretaerin
zeigt auf die Angestellte
.chef
zeigt auf den Unternehmer
, genauso wie steve
.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.
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
.
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.
Abbildung 9.9 Vergleich normale Variablen und schwache Referenzen. Links ist »johanna« eine normale Variable, rechts eine »schwache Referenz«
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.
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.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.
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:
var
und let
)
die richtige Wahl. Wenn Sie sich nicht sicher sind, so bleiben Sie hierbei.chef
gelegentlich nicht existieren muss), so machen Sie sie zu einem optionalen Typ.weak
sein.unowned
verwenden. Dieser Fall tritt in der Praxis allerdings am seltensten auf. Dieses Konstrukt ist eine Besonderheit 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.
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.
struct
dann eine vollwertige Alternative zur class
, um Objekte zu beschreiben. Die Bedeutung ist dadurch eine völlig andere als in . In der Programmiersprache Objective-C gibt es nur die struct
-Blöcke der Programmiersprache C. Es ist daher wichtig, sich darüber im Klaren zu sein, dass eine struct
in anderen Programmiersprachen etwas anderes ist als in Swift.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!
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.
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.
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.