4.6 Analogien und Unterschiede zwischen Listen und Skript-Objekten

Ein sehr nützliches Hilfsmittel bei der Programmierung komplexerer Probleme stellen in Lingo die Listen dar. Mit Hilfe einer Liste können in einer Variablen beliebig viele Werte gespeichert werden. Diese Werte müssen nicht alle vom gleichen Typ sein, sondern es können beliebige unterschiedliche Typen in einer Liste gespeichert werden, also auch andere Listen oder Objekte. Eine Liste stellt ein vorgefertigtes Lingo-Objekt dar. Sie kann Daten speichern und ihr sind bestimmte Methoden zugeordnet.
.
Im folgenden sollen die Analogien und Unterschiede zwischen Listen und Skript-Objekten am Beispiel linearer Listen gezeigt werden; dasselbe gilt aber auch für Property- bzw. Eigenschaftslisten, auf deren Besonderheit im Anschluss eingegangen werden soll.

4.6.1 Kopieren und Vergleichen von Listen und Objekten
4.6.2 Analogie zwischen Objekten und Eigenschaftslisten
4.6.3 Objekte und Listen als Parameter bei Handleraufrufen

4.6.1 Kopieren und Vergleichen von Listen und Objekten

Bei einer Liste werden in einer Variablen nicht die eigentlichen Werte gespeichert, sondern eine Referenz auf die Datenstruktur. Dies hat sie mit Objekten gemeinsam. Deswegen werden bei einer erneuten Zuweisung nicht die Werte kopiert, sondern nur die Referenz.

listeA = ["Alpha", "Beta"]
listeB = listeA -- hier wird nur die Referenz kopiert
listeA.add("Gamma") -- Liste A wird ein Wert hinzugefügt
put listeA
-- ["Alpha", "Beta", "Gamma"]
put listeB
-- ["Alpha", "Beta", "Gamma"]

Da nur die Referenz kopiert wird, bezieht sich die Variable listeB auf ein und dieselbe Liste. Um wirklich die Werte zu kopieren, muss man den Befehl duplicate() benutzen.

listeB = listeA.duplicate() -- es wird neue Liste mit den gleichen
Werten wie listeA erzeugt.
listeA.add("Delta")
put listeA
-- ["Alpha", "Beta", "Gamma", "Delta"]
put listeB
-- ["Alpha", "Beta", "Gamma"]

Nun bleibt listeB von der Änderung unbehelligt. listeA und listeB beziehen sich auf zwei unterschiedliche Listen.

Dasselbe gilt auch für Objekte. Als Beispiel soll nochmal das Parent-Skript GrussObjekt dienen:

objektA = script("GrussObjekt").new("Hallo")
objektB = objektA

Nun wird die Eigenschaft mGrussText von objektB verändert:

objektB.setGruss("Servus.")

objektA und objektB referenzieren dasselbe Objekt, deswegen verhalten sich beide gleich:

objektB.hallo()
-- "Servus."
objektA.hallo()
-- "Servus."

Eine direkte Ausgabe der Inhalte der Variablen verdeutlicht dies noch einmal:

put objektA
-- <offspring "GrussObjekt" 3 1000dbc>
put objektB
-- <offspring "GrussObjekt" 3 1000dbc>

Objekte haben leider keine Methode, die automatisch eine Kopie erstellt; diese muss selbst implementiert werden.

on duplicate me
return script("GrussObjekt").new(mGrusstext)
end duplicate

Der Methodenaufruf sieht dann folgendermaßen aus:

objektB = objektA.duplicate()

objektB referenziert jetzt ein neues Objekt; es verändert sich nicht automatisch mit objektA.

Wenn eine duplicate-Methode für ein Objekt implementiert wird, dessen Eigenschaften weitere Objekte oder Listen referenzieren, ist darauf zu achten, ob ein sogenanntes shallow cloning oder deep cloning benötigt wird. Beim shallow cloning werden nur Objektreferenzen kopiert; Eigenschaften, die auf Objekte verweisen, verweisen auch in der Kopie auf dasselbe Objekt. Beim deep cloning wird wiederum bei Listen und Objekten die duplicate-Methode aufgerufen, die natürlich vorhanden sein muss. Objekt und Kopie verweisen somit auf keine gemeinsamen Daten. Wie "tief" die Kopie ausgeführt werden muss, liegt in der Hand des Programmierers. Wird zu flach kopiert, können unvorhergesehene Seiteneffekte auftreten. Wird zu tief kopiert, verschwendet man unter Umständen unnötig viel Speicher. Deswegen würde es auch wenig Sinn machen, wenn Objekte diese Methode von Haus aus implementieren würden, da für verschiedene Objekte unterschiedlich tief kopiert werden muss.7

Ein weiterer Unterschied ist die abweichende Behandlung des Vergleichsoperators "=". Bei einer Liste werden tatsächlich nur die enthaltenen Werte verglichen, auch wenn es sich nicht um dieselbe Liste handelt.

listeA = ["eins", "zwei", "drei"]
listeB = ["eins", "zwei", "drei"]
put listeA = listeB
-- 1

Die beiden Listen werden als gleich angesehen, da sie die gleichen Werte enthalten. Lingo geht dabei so vor, dass jeder Wert in der Liste mit dem entsprechenden Wert aus der anderen Liste verglichen wird. Dadurch funktioniert der Vergleich z.B. auch mit verschachtelten Listen.

Beim Vergleich von Objekten wird nur geprüft, ob auf die gleiche Instanz verwiesen wird.

objektA = script("GrussObjekt").new("Hallo")
objektB = script("GrussObjekt").new("Hallo")
put objektA = objektB
-- 0

Obwohl die Eigenschaften beider Objekte gleich sind, werden die Objekte als nicht gleich angesehen, da es sich um unterschiedliche Instanzen handelt. Um die Objekte zu vergleichen, muss man eine eigene Methode schreiben.

on equals me, objekt
return (mGrussText = objekt.getGruss())
end equals

Der Aufruf sieht dann wie folgt aus:

put objektA.equals(objektB)
-- 1

Somit wird festgestellt, dass die Objekte identische Werte enthalten. Die Methode geht jedoch davon aus, dass die zwei vergleichenden Objekte vom gleichen Parent-Skript abgeleitet sind. Dies lässt sich in Lingo leider nicht zuverlässig prüfen. Es ist zwar theoretisch möglich, die Objektreferenz in einen String umzuwandeln und den Skriptnamen auszuwerten, doch ist dieser nur in dem Film gültig, indem das Objekt instantiiert wurde. Beim wechseln in einen neuen Movie steht an dieser Stelle der Name des Darstellers, der in der Besetzung die gleiche Position belegt wie das ursprüngliche Skript.

4.6.2 Analogie zwischen Objekten und Eigenschaftslisten

Neben linearen Listen gibt es in Lingo als zweiten Listentyp die Eigenschaftslisten. Eine Eigenschaftsliste enthält an einer Indexposition nicht einen Wert, sondern ein Wertepaar, nämlich eine Eigenschaft (Schlüssel) und einen Wert. Es kann zum Beispiel abgefragt werden, welcher Eigenschaft welcher Wert zugeordnet ist. Eigenschaft und Wert können einen beliebigen Typ haben. Wenn die Eigenschaft vom Typ Symbol ist, lassen sich die Werte einer Eigenschaftsliste wie Eigenschaften eins Objektes abfragen. Im folgenden soll ein fiktiver Datensatz in Form einer Eigenschaftsliste dies verdeutlichen:

datensatz = [#vorname: "Otto", #nachname: "Normalverbraucher"]
put datensatz.vorname -- Ausgabe des Wertes, der mit der Eigenschaft
"#vorname" verbunden ist.
-- "Otto"
-- jetzt wird die Eigenschaft "beruf" hinzugefügt
datensatz.addProp(#beruf, "Programmierer")
put datensatz
[#vorname: "Otto", #nachname: "Normalverbraucher", #beruf: "Programmierer"]
-- wieviele Eigenschaft-Wert-Paare enthält "datensatz"?
put datensatz.count()
-- 3

Zum Vergleich soll jetzt das Parent-Skript DatensatzObjekt geschrieben werden, das denselben Datensatz als Objekt speichern soll:

property mVorname, mNachname, mBeruf

on new me, vorname, nachname, beruf
mVorname = vorname
mNachname = nachname
mBeruf = beruf
return me
end new

Wir erzeugen einen Datensatz:

datensatzObj = script("DatensatzObjekt").new("Otto",
"Normalverbraucher","Programmierer")

Dieses Objekt kann mit den gleichen Befehlen nach seinen Eigenschaften abgefragt werden wie eine Eigenschaftsliste:

-- wieviele Eigenschaften hat das Datensatzobjekt?
put datensatzObj.count()
-- 3
put datensatzObj.getPropAt(1) -- gebe den Namen der ersten
Eigenschaft aus
-- #mNachname
put datensatzObj[1] -- gebe den Wert der ersten Eigenschaft aus
-- "Normalverbraucher"

Die Reihenfolge der Eigenschaften entspricht dabei nicht der Reihenfolge ihrer Definition, sondern wird von Director festgelegt. Als Programmierer sollte man sich also nicht darauf verlassen, die Eigenschaften über den Index anzusprechen.

Mit den Listenbefehlen lässt sich Beispielsweise der Wert eines Objektes während des Debuggens im Nachrichtenfenster anzeigen:

on debug me
returnString = "Objekt " & string(me)& RETURN
repeat with i = 1 to me.count()
returnString = returnString & me.getPropAt(i)
& ": " & me[i] & RETURN
end repeat
put returnString
end debug

Im Nachrichtenfenster können dann die Werte der Eigenschaften des Objektes eingesehen werden:

datensatz.debug()
-- "Objekt <offspring "test" 4 1000e0c>
mBeruf: Programmierer
mNachname: Normalverbraucher
mVorname: Otto
"

Der Vorteil dieser Methode ist, dass sie nicht angepasst werden muss, wenn dem Objekt später noch Eigenschaften hinzugefügt werden sollten.

Folgende Listenbefehle können auch auf Objekte angewendet werden:

getProp(#Eigenschaft) bzw. [#Eigenschaft]

Liefert den Wert der angegebenen Eigenschaft.

getPropAt(Index) bzw. [Index] Liefert den Wert der Eigenschaft mit dem angegebenen Index.
setAProp(#Eigenschaft) / setProp(#Eigenschaft) bzw.
[#Eigenschaft] = Wert
Setzt den Wert der angegebenen Eigenschaft. Existiert die Eigenschaft nicht, wird sie bei setAProp / setProp hinzugefügt.
setAt(#Index, Wert) bzw. [#Index] = Wert

Setzt die Eigenschaft mit dem angegebenen Index.

count() Gibt die Anzahl der Eigenschaften zurück.

4.6.3 Objekte und Listen als Parameter bei Handleraufrufen

Die Tatsache, dass Objekte und Listen als Referenz verwaltet werden, spiegelt sich in der Art und Weise wider, wie sie sich bei Funktionsaufrufen verhalten. Primitive Datentypen, das sind in Director Integer, Float, String und Symbol, werden immer "by value", d.h. als Wert, Objekte und Listen immer "by reference", d.h. als Referenz übergeben. In der Praxis bedeutet dies, dass ein Handler bzw. eine Methode bei primitiven Datentypen immer mit einer Kopie der Daten arbeitet, bei Objekten und Listen immer mit den tatsächlichen Daten selbst. Manipulationen der Daten innerhalb des Handlers wirken sich also auch auf das restliche Programm aus.

Die Methode sortiertAusgeben soll den Inhalt einer Liste in alphabetischer Reihenfolge ausgeben:

on sortiertAusgeben liste
liste.sort()
repeat with eintrag in liste
put eintrag
end repeat
end ausgeben

Wenn wir den Handler aufrufen, verändert der den Inhalt der übergebenen Variablen:

obstListe = ["Erdbeere", "Birne", "Apfel"]
sortiertAusgeben(obstListe)
-- "Apfel"
-- "Birne"
-- "Erdbeere"
put obstListe
-- ["Apfel ", "Birne", " Erdbeere "]

Um dies zu vermeiden, muss im Handler mit einer Kopie gearbeitet werden.

on sortiertAusgeben liste
listeKopie = liste.duplicate()
listeKopie.sort()
repeat with eintrag in listeKopie
put eintrag
end repeat
end sortiertAusgeben

Der neue Handler lässt den Inhalt der Liste unangetastet.

4.5 Beispiel 1: Zini 4.7 Beispiel 2: Spielstände speichern mit Listen und Objekten