home
***
CD-ROM
|
disk
|
FTP
|
other
***
search
/
Global Amiga Experience
/
globalamigaexperience.iso
/
compressed
/
development
/
clusterdemo.dms
/
clusterdemo.adf
/
Sprachreport.lha
/
OOPCluster
< prev
Wrap
Text File
|
1993-03-08
|
28KB
|
861 lines
Objektorientiertes Programmieren in Cluster
1. Grundlegende Struktur
Objekte entsprechen in Cluster in vielen Eigenschaften den Records. Dies
drückt sich auch in der Definition aus:
[1] $$ObjectDefinition = OBJECT
{ident{,ident} : TypeDefinition;}
END
TYPE
Node = POINTER TO NodeObj;
NodeObj = OBJECT
prev,next : Node;
END;
Der deutlichste syntaktische Unterschied liegt im Schlüsselwort "OBJECT" an
der Stelle von "RECORD". Der wichtigste semantische Unterschied besteht
darin, daß ein Objekt nicht nur einen statischen Typ, sondern auch einen
dynamischen Typ besitzt (ähnlich des Records in Oberon, aber weitergehend).
Dies wird bei Vererbungen deutlich:
TYPE
BigNode = POINTER TO OBJECT OF Node;
name : STRING(10);
END;
VAR n : Node;
b : BigNode;
...
n:=b
...
Die Variablen "n" und "b" haben zwei verschiedene statische Typen, nämlich
"Node" und "BigNode". Sie haben nach der Zuweisung allerdings den selben
dynamischen (d.h. zur Laufzeit) Typen, "BigNode". Dies kann auch durch den
relationalen Operator IS geprüft werden:
...
IF n IS BigNode THEN ... END;
...
Wären "BigNode" und "Node" Records gewesen, wäre der dynamische Typ
"BigNode" bei der Zuweisung an n verloren gegangen. Eine umgekehrte
Zuweisung "b:=n" wäre illegal oder zumindest sehr fraglich.
Bei Objekten wird zu jedem Objekt ein dynamischer Typ mitgeführt, so daß
die Legalität einer Zuweisung "b:=n" überprüft werden kann. Auch eine
Typumwandlung wie:
...
WriteString(BigNode(n).name);
...
kann ordnungsgemäß ausgeführt werden, da der Compiler automatisch einen
Typtest ins Programm einfügt (dies kann bei fertigen Programmen durch
"$$TypeChk:=FALSE" unterbunden werden). Wie sich aber noch zeigen wird, ist
eine derartige Zuweisung dank der Verwendung dynamischen Bindens allerdings
sehr selten.
Der Typ eines Objekts wird auch als Klasse bzw. Klassenzugehörigkeit
bezeichnet.
Einen Nachteil haben Objekte gegenüber Records, Objekte können nicht als
Variable existieren, nur Zeiger auf Objekte sind erlaubt.
Der statische Typ eines Objektes ist immer durch den Zeiger auf dieses
gegeben, der dynamische Typ (also der, den das Objekt nun wirklich hat) ist
im Objekt selbst kodiert. Der dynamische Typ eines Zeigers auf ein Objekt
kann sich während der Programmausführung durch Zuweisungen ändern, der
statische Typ ist immer fest und durch die Typdefinition im Programmtext
festgelegt. Der Compiler achtet darauf, daß der dynamische Typ eines
Objekts immer dem statischen entspricht, oder aber ein Nachfolger davon ist.
2. Einfacherben
Einfacherben gehorcht der selben Syntax und Semantik wie bereits von Records
bekannt:
[2] $$ObjectDefinition = OBJECT [OF qualident;]
{ident{,ident} : TypeDefinition;}
END
TYPE
Fahrzeug = POINTER TO OBJECT
geschwindigkeit : REAL;
gewicht : REAL;
END;
Flugzeug = POINTER TO OBJECT OF Fahrzeug;
triebwerke : [0..12];
END;
Auto = POINTER TO OBJECT OF Fahrzeug;
raeder : [1..8];
END;
VW = POINTER TO OBJECT OF Auto;
typ : (Kaefer,Golf,Polo,Passat,Jetta...)
END;
Der Nachfolger erbt alle Elemente und Fähigkeiten seines Vorgängers und kann
diesem neue hinzufügen. Eine Zuweisungskompatibilität an seinen Vorgänger
ist uneingeschränkt gegeben, der umgekehrte Fall wird durch einen Typcheck
während der Laufzeit gesichert.
Beispiele für Zuweisungen:
VAR fz,fz2 : Fahrzeug;
fl : Flugzeug;
au,au2 : Auto;
vw : VW;
fz:=fl => richtig (Hierbei ändert sich der dynamische Typ von fz zu
"Flugzeug", ist also ein Nachfolger des statischen
Typs "Fahrzeug".)
fz:=au => richtig
fz:=vw => richtig
au:=vw => richtig
fl:=fz => laufzeittest
vw:=au => laufzeittest
fl:=au => falsch
fl:=vw => falsch
vw:=fl => falsch
Die Zugehörigkeit zu einer Klasse kann während der Laufzeit durch den
relationalen Operator "IS" geprüft werden. Der Operator ist nur für Objekte
gestattet, deren statischer Typ in linearer Nachfolge-/Vorgängerbeziehung
zur getesteten Klasse stehen.
Beispiele für Tests:
fz sei Fahrzeug
fz2 sei VW
fl sei Flugzeug
au sei Auto
au2 sei VW
vw sei VW
fz IS Fahrzeug : statisch wahr
fz2 IS Fahrzeug : statisch wahr
vw IS VW : statisch wahr
vw IS Fahrzeug : statisch wahr
fz2 IS Auto : dynamisch wahr
fz2 IS VW : dynamisch wahr
au2 IS VW : dynamisch wahr
fz2 IS Flugzeug : dynamisch falsch
au IS VW : dynamisch falsch
au2 IS Flugzeug : fehlerhafte Anweisung, da der statische Typ von
au2 (also Auto) kein Nachfolger von Flugzeug ist,
also sein dynamischer Typ unmöglich ein Nachfolger
von Flugzeug sein kann. (Die Funktion könnte auch
immer FALSE zurückgeben, doch deutet die Verwendung
dieses Operators in diesem Zusammenhang eher auf
einen Programmfehler hin, deshalb ist dies Verboten).
Der statische Typ eines Objektes kann seinem dynamischen Typ für einen
Bereich im Programm angepaßt werden:
Auto(fz).raeder:=4;
oder für längere Zeit:
WITH VW(fz) DO
fz.raeder:=4;
fz.typ:=Kaefer;
END;
Damit ist im allgemeinen ein Laufzeitcheck verbunden (in wie weit dieser
Check bei einer Mehrpassimplementierung des Compilers vermieden werden
kann, wird noch untersucht). Eventuell kann dies auch durch die Einführung
einer TYPEKEY Struktur wie:
IF TYPEKEY fz
OF VW THEN
fz.typ:=Kaefer
END
OF Flugzeug THEN
fz.triebwerke:=4
END
ELSE
fz.gewicht:=-1.0; | Antigravantrieb :-)
END
Die Implementierung steht allerdings noch aus. Auch ist der Sinn dieser
Struktur bei der Verwendung dynamischen Bindens ein wenig fragwürdig.
3. Methoden auf Objekte
Auch (oder gerade) auf Objekte können Methoden erklärt werden. Diese müssen
allerdings bereits bei der Typdeklaration angemeldet werden (Begründung
später). Die Methoden müssen allerdings in einem Definitionsmodul nicht noch
einmal angegeben werden, es reicht hierbei die Angabe in der Typdefinition.
[3] $$ObjectDefinition = OBJECT [OF qualident;]
{
(ident{,ident} : TypeDefinition;)|
(METHOD ident [ FormalParameter ];)
}
END
$$MethodImplementation = METHOD ident.ident [FormalParameter ];
Block ident;
Beispiel:
TYPE
Lebewesen = POINTER TO OBJECT
alter : INTEGER;
METHOD Altere(um : INTEGER := 1);
END;
METHOD Lebewesen.Altere(um : INTEGER);
BEGIN
...
END Altere;
Der Methodenaufruf erfolgt analog zu dem für Records:
VAR le : Lebewesen;
...
le.Altere;le.Altere(10);
...
Die Elemente (Instanzvariablen) eines Objektes sind in der Implementation
der Methode unqualifiziert bekannt:
METHOD Lebewesen.Altere(um : INTEGER);
BEGIN
INC(alter,um);
END Altere;
Das Objekt selbst, für das die Methode aufgerufen wurde, ist unter dem
Bezeichner SELF verfügbar:
TYPE
Lebewesen = POINTER TO OBJECT
alter : INTEGER;
METHOD Altere(um : INTEGER := 1);
METHOD AltereStark;
END;
METHOD Lebewesen.AltereStark;
BEGIN
SELF.Altere(10);
END AltereStark;
3.1. Methoden und Erben (dynamisches Binden)
Methoden von Objekten können beim Erben redefiniert, d.h. durch neue
Methoden, die sich dann auf die geerbte Klasse beziehen, ersetzt werden.
Die Methode muß hierbei bei der Definition des neuen Typs mit angegeben
werden. Der Typ der neuen Methode muß mit dem der bestehenden
übereinstimmen.
TYPE
Mensch = POINTER TO OBJECT OF Lebewesen;
haarfarbe : (schwarz,dunkel,grau,weiss);
METHOD Altere(um : INTEGER);
END;
METHOD Mensch.Altere(um : INTEGER);
BEGIN
INC(alter,um);
IF KEY alter
OF 0.. 49 THEN haarfarbe:=schwarz END
OF 50.. 59 THEN haarfarbe:=dunkel END
OF 60.. 69 THEN haarfarbe:=grau END
ELSE
haarfarbe:=weiss
END;
END Altere;
Welche der Methoden wärend der Laufzeit ausgeführt werden, bestimmt der
dynamische Typ eines Objekts (im Gegensatz zu Methoden bei Records, wo der
statische Typ entscheidend ist). So würde also für ein Objekt mit dem
dynamischen Typ Mensch und dem statischen Typ Lebewesen immer die Methode
mit der Haarfarbe aufgerufen.
Beispiel:
VAR le : Lebewesen;
me : Mensch;
...
le:=me; | Lebewesen ist sicher ein Mensch
le.Altere;
...
Auch der Aufruf von "AltereStark" würde letztendlich in einem Aufruf der
Haarfarbmethode gipfeln, obwohl bei der Definition dieser Methode von der
Existenz von Menschen (und der damit verbundenen Haarprobleme) noch nichts
bekannt war.
Mit dem Schlüsselwort "SUPER" haben redefinierte Methoden Zugriff auf
Methoden der statischen Vorgängerklasse. Dies ist sinnvoll, falls die neue
Methode eigentlich eine Erweiterung der bestehenden Methode darstellt.
METHOD Mensch.Altere(um : INTEGER);
BEGIN
SUPER.Altere(um);
IF KEY alter
OF 0.. 49 THEN haarfarbe:=schwarz END
OF 50.. 59 THEN haarfarbe:=dunkel END
OF 60.. 69 THEN haarfarbe:=grau END
ELSE
haarfarbe:=weiss
END;
END Altere;
3.2. Aufgeschobene (deferred) Methoden und Containerklassen
Eine aufgeschobene Methode ist eine solche, die zwar vereinbart, aber
absichtlich nicht implementiert wird. Ein Aufruf einer derartigen Methode
führt zu einem Laufzeitfehler.
[4] $$ObjectDefinition = OBJECT [OF qualident;]
{
(ident{,ident} : TypeDefinition;)|
([DEFERRED] METHOD ident [ FormalParameter ];)
}
END
Aufgeschobene Methoden können später durch wirklich existierende Methoden
redefiniert werden. Dies ergibt erst den Sinn dieser Methoden. Sie dienen
dazu, ein Objekt mit Fähigkeiten zu schaffen, die auf anderen Fähigkeiten
basieren, die sehr abstrakt gehalten werden können.
TYPE
Stream = POINTER TO OBJECT
termChar : CHAR;
DEFERRED METHOD Read(VAR c : CHAR);
DEFERRED METHOD Write(c : CHAR);
METHOD WriteString(REF s : STRING);
METHOD ReadString(VAR s : STRING);
METHOD WriteLn;
END;
METHOD Stream.WriteString(REF s : STRING);
VAR i : INTEGER;
BEGIN
FOR i:=0 TO PRED(s.len) DO
SELF.Write(s.data[i]);
END;
END WriteString;
METHOD Stream.WriteLn;
BEGIN
SELF.Write(ASCII.lf);
END WriteLn;
METHOD Stream.ReadString(VAR s : STRING);
VAR i : INTEGER := 0;
c : CHAR;
BEGIN
SELF.Read(c);
WHILE c NOT OF ASCII.lf," ",ASCII.tab DO
ASSERT(i<s'MAX,RangeViolation);
s.data[i]:=c;
INC(i);
SELF.Read(c);
END;
termChar:=c;
s.data[i]:=ASCII.null;
END ReadString;
Ein Objekt dieser Klasse wäre ziemlich sinnlos, da jeder Aufruf einer seiner
Methoden zu einem Laufzeitfehler führen würde. Ein Erbe dieser Klasse kann
allerdings durch Implementation von "Read" und/oder "Write" voll funktional
werden. Es erbt auch alle erweiterten Methoden wie "WriteString" etc.
Erst durch dieses Erben entsteht eine funktionsfähige Klasse.
TYPE
DosStream = POINTER TO OBJECT OF Stream;
fh : Dos.FileHandlePtr;
METHOD Read(VAR c : CHAR);
METHOD Write(c : CHAR);
END;
METHOD DosStream.Read(VAR c : CHAR);
VAR i : INTEGER;
BEGIN
i:=Dos.Read(fh,c'PTR,1);
IF KEY i
OF 0 THEN RAISE(Dos.EOF) END
OF 1 THEN RAISE(Dos.ReadError) END
END
END Read;
METHOD DosStream.Write(c : CHAR);
BEGIN
ASSERT(Dos.Write(fh,c'PTR,1)=1,Dos.WriteError);
END Write;
Ein weiterer Vorteil liegt darin, daß Objekte dieser Klasse zuweisungsfähig
an Objekte (bzw. Objektzeiger) der ursprünglichen Klasse sind.
Beispiel: ein Filter, der alle Nennungen von "Gott" in "jenes höhere Wesen,
das wir verehren" (frei nach "Murkes gesammeltes Schweigen") ersetzt.
PROCEDURE MurkeFilter(in,out : Stream);
VAR s : STRING(100);
BEGIN
TRY
LOOP
in.ReadString(s);
IF Strings.Equal(s,"Gott") THEN
out.WriteString("jenes höhere Wesen, das wir verehren")
ELSE
out.WriteString(s);
END;
out.Write(in.termChar);
END;
EXCEPT
OF Dos.EOF THEN END
END;
END MurkeFilter;
Dieser Filter arbeitet mit allen Arten von funktionsfähigen Streams
zusammen, obwohl er kein Wissen über deren vollständige Implementierung
trägt.
Im obigen Beispiel könnte die Methode "WriteString" in "DosStream" aus
Performancegründen ebenfalls überdefiniert werden:
METHOD DosStream.WriteString(REF s : STRING);
BEGIN
ASSERT(Dos.WriteString(fh,s.data'PTR,s.len)=s.len,Dos.WriteError);
END WriteString;
Grundsätzlich kann jede Methode bei jedem Erbvorgang durch eine neue
ersetzt werden.
Eine Containerklasse ist eine Klasse, die keine eigentliche Funktionalität
besitzt, und nur durch Beerben und Redefinition der aufgeschobenen Methoden
sinnvoll wird.
3.3. Die Methoden "Construct" und "Destruct"
Häufig benötigen Objekte Hilfsmittel, um ihre Fähigkeiten zu erhalten (z.B.
das "FileHandle" des Objektes "DosStream"). Diese Hilfsmittel müssen bei
der Erzeugung des Objektes alloziert bzw. initialisiert und bei der
Vernichtung des Objektes wieder freigegeben werden. Dazu dienen die
parameterlosen Methoden "Construct" und "Destruct". Sie werden bei der
Erzeugung bzw. Vernichtung eines Objektes aufgerufen.
Beispiel:
TYPE
DosStream = POINTER TO OBJECT OF Stream;
fh : Dos.FileHandlePtr;
METHOD Read(VAR c : CHAR);
METHOD Write(c : CHAR);
METHOD Destruct;
END;
METHOD DosStream.Destruct;
BEGIN
IF fh#NIL THEN
Dos.Close(fh);fh:=NIL
END;
END Destruct;
oder ein Filterobjekt, das jedem ASCII-Zeichen ein anderes zuordnen kann:
TYPE
Filter = POINTER TO OBJECT;
table : POINTER TO ARRAY CHAR OF CHAR;
METHOD SetFilter(from,to : CHAR);
METHOD Translate(c : CHAR):CHAR;
METHOD Construct;
METHOD Destruct;
END;
METHOD Filter.SetFilter(from,to : CHAR);
BEGIN
table[from]:=to
END SetFilter;
METHOD Filter.Translate(c : CHAR):CHAR;
BEGIN
RETURN table[c]
END Translate;
METHOD Filter.Construct;
VAR c : CHAR;
BEGIN
Resources.New(table);
FOR c:=CHAR'MIN TO CHAR'MAX DO
table[c]:=c;
END;
END Construct;
METHOD Filter.Destruct;
BEGIN
Resources.Dispose(table)
END Destruct;
Diese Methoden haben zwei große Unterschiede zu allen anderen normalen
Methoden; erstens werden sie vom Laufzeitsystem aufgerufen, zweitens wird
nicht die jüngste Methode aufgerufen, sondern alle, die sich im Stammbaum
angesammelt haben. Eine Con/Destruct Methode sollte sich also nur um Dinge
kümmern, die direkt mit der aktuellen Ausbaustufe des Objekts
zusammenhängen. Spezielle Initialisierungen sollten im "CONSTRUCTOR"
vorgenommen werden (siehe 4).
4. Erzeugung und Vernichtung von Objekten
Da bei der Erzeugung von Objekten im allgemeinen Initialisierungen
vorgenommen werden müssen, scheidet ein einfaches Allozieren von vorneherein
aus. Objekte können auf zwei Arten erzeugt werden, durch einen Constructor
(eine besondere Methode) oder durch die Standardprozedur "NEW" (evtl. nicht
mehr lange). Die Vernichtung erfolgt analog durch einen Destructor bzw.
"DISPOSE".
[5] $$ObjectDefinition = OBJECT [OF qualident;]
{
(ident{,ident} : TypeDefinition;)|
([DEFERRED]
(METHOD|CONSTRUCTOR|DESTRUCTOR)
ident [ FormalParameter ];)
}
END
Beispiel:
TYPE
DosStream = POINTER TO OBJECT OF Stream;
fh : Dos.FileHandlePtr;
CONSTRUCTOR Create(REF name : STRING);
CONSTRUCTOR Open(REF name : STRING);
DESTRUCTOR Close;
METHOD Read(VAR c : CHAR);
METHOD Write(c : CHAR);
METHOD Destruct;
END;
METHOD DosStream.Create(REF name : STRING);
BEGIN
fh:=Dos.Open(name,Dos.newFile)
ASSERT(fh#NIL,Dos.ObjectNotFound);
END Create;
METHOD DosStream.Open(REF name : STRING);
BEGIN
fh:=Dos.Open(name,Dos.oldFile)
ASSERT(fh#NIL,Dos.ObjectNotFound);
END Open;
METHOD DosStream.Close;BEGIN END Close;
Wird für ein (im allgemeinen noch nicht existierendes Object) ein
Constructor aufgerufen, wird dieses erzeugt (anhand des statischen Typs).
Danach wird die Constructor Methode (in diesem Fall z.B. "Open") aufgerufen,
die weitere Initialisierungen vornehmen kann. Alle Instanzvariablen eines
Objektes werden mit 0, NIL, FALSE etc. vorinitialisiert. Die Vernichtung
läuft umgekehrt ab, erst wird der Destructor aufgerufen, dann das Objekt
vernichtet. Im obigen Beispiel ist "Close" mehr pro forma deklariert, da
die eigentliche Vernichtung des FileHandles durch die Methode "Destruct"
vorgenommen wird.
Eine Klasse kann beliebig viele Constructoren besitzen, auch dürfen diese
als einzige Methode durch Constructoren mit anderen Parametern überdefiniert
werden, da diese statisch (aus dem statischen Typ) ermittelt werden.
Beispiel:
VAR in,out : DosStream;
BEGIN
in.Open("Rede die 1.");
out.Create("Rede die 2.");
MurkeFilter(in,out);
out.Close;
in.Close
END ...
Die Erzeugung mit "NEW" und "DISPOSE" sollte nur für sehr einfache Objekte
verwendet werden, ihre weitere Existenz ist außerdem fraglich.
in:=NEW(DosStream);
out:=NEW(DosStream);
DISPOSE(in);
DISPOSE(out);
Eine weitere Möglichkeit, ein Objekt zu erzeugen, ist ein bestehendes zu
verdoppeln (Klonen). Dies kann durch die Standardfunktion "CLONE" geschehen.
(Allerdings wird wohl in Zukunft eine andere Technik verwendet werden).
p:=CLONE(q);
4.1. Der Unterschied zwischen "Con-"/"Destruct" und "CON-"/"DESTRUCTOREN"
Die "CON-"/"DESTRUCTOREN" werden vom Programmierer zur Erzeugung bzw.
Vernichtung eines Objektes eingesetzt. Da es mehrere Arten gibt, wie ein
Objekt erzeugt werden kann, und auch evtl. Parameter für die
Initialisierung benötigt werden, kann eine Klasse mehrere "CON-"/
"DESTRUCTOREN" mit verschiedenen Parametern besitzen.
Die Methoden "Con-"/"Destruct" werden vom Laufzeitsystem während der
Initialisierung und Vernichtung aufgerufen. Sie dienen einer grundlegenden
Initialisierung bzw. einer Freigabe von Betriebsmitteln, wenn ein Objekt
unter Notfallbedingungen (z.B. Laufzeitfehler, verlassen eines Kontexts).
Sie können auch die Verwaltung von Betriebsmitteln an übergeordnete
Schichten verheimlichen.
5. Mehrfacherben (multiple inheritance)
Oft ist es sinnvoll, wenn eine Klasse nicht nur von einer Vorgängerklasse
sondern von beliebig vielen erben kann.
[6] $$ObjectDefinition = OBJECT [OF qualident{,qualident};]
{
(ident{,ident} : TypeDefinition;)|
([DEFERRED]
(METHOD|CONSTRUCTOR|DESTRUCTOR)
ident [ FormalParameter ];)
}
END
Beispiel:
TYPE
InputStream = POINTER TO OBJECT
termChar : CHAR;
DEFERRED METHOD Read(VAR c : CHAR);
METHOD ReadString(VAR s : STRING);
END;
OutputStream = POINTER TO OBJECT
DEFERRED METHOD Write(c : CHAR);
METHOD WriteString(s : STRING);
METHOD WriteLn;
END;
InOutStream = POINTER TO OBJECT OF InputStream,OutputStream;
END;
Ein Objekt der Klasse "InOutStream" vereinigt die Fähigkeiten eines
"InputStream" mit denen eines "OutputStream" und ist auch
zuweisungskompatibel zu beiden.
Beispiele für legale Anweisungen/Ausdrücke
VAR in : InputStream;
out : OutputStream;
io : InOutStream;
...
in:=io;
out:=io;
IF in IS InOutStream THEN ...
IF out IS InOutStream THEN ...
...
MurkeFilter(io,out);
falls dieser definiert wird als
PROCEDURE MurkeFilter(in : InputStream;
out : OutputStream);
...
5.1. Probleme beim Mehrfacherben
Schwierig wird Mehrfacherben, wenn in der Hierarchie gleiche Bezeichner
auftauchen. Dies kann auf zwei Arten geschehen, erstens, in einem der Väter
taucht ein Bezeichner auf, der auch in einem anderen existiert, oder
zweitens, eine Klasse ist zweimal Erbe derselben Klasse. (ACHTUNG, der
Compiler führt zur Zeit noch keinen Test bei Mehrfacherben aus, es kann
also unerkannt zu Problemen kommen. Dieser Mangel wird demnächst behoben.)
Diesem Problem kann durch Qualifizierung beigekommen werden, dabei wird
einem oder mehreren Elternteilen ein qualifizierender Bezeichner
beigestellt, der bei der Referenzierung ihrer/s Elemente benutzt werden
kann.
[7] $$ObjectDefinition = OBJECT [OF qualident [AS ident]
{,qualident [AS ident]};]
{
(ident{,ident} : TypeDefinition;)|
([DEFERRED]
(METHOD|CONSTRUCTOR|DESTRUCTOR)
ident [ FormalParameter ];)
}
END
Beispiel: Knoten für eine Kreuzliste:
TYPE
DoubleNode = POINTER TO OBJECT OF Node AS h,Node AS v; END;
VAR dn : DoubleNode;
Auf die Elemente der "DoubleNode" kann nun über die qualifizierenden
Bezeichner ".h" und ".v" zugegriffen werden. (Die volle Mächtigkeit kommt
erst durch Generizität zum Tragen.)
Eine andere Lösung wäre die, daß wenn eine Klasse in der Hierarchie mehrmals
auftaucht, sie dennoch nur einmal vorhanden ist. Dies wäre sehr sinnvoll für
verschiedene Anwendungen, wirft aber neue Probleme auf, die im Moment eine
Implementation noch verzögern (z.B. wenn aus der bestehenden Hierarchie zwei
verschiedene Methodenredefinitionen für diese Klasse existieren).
6. Objekte und Generizität
Generizität bedeutet, daß Module mit abstrakten Datentypen definiert werden
können, die dann für tatsächlich existierende Typen ausgeprägt werden. Die
aktuellen Typen können dabei durch Forderungen (im allgemeinen geschieht
dies durch eine Nachfolger-Vorgänger Bedingung) eingegrenzt werden.
Es stellt sich die Frage, wozu generische Module, wenn doch der Typ eines
Objektes auch dynamisch ermittelt werden kann und somit also keine illegalen
Zuweisungen oder Verwendungen möglich sind.
Die Antwort ist einfach, Generizität erhöht die statische Sicherheit eines
Programmes, vermeidet somit sowohl unnötige Typchecks als auch mögliche
Laufzeitfehler.
Die Regeln für generische Module sind im Prinzip dieselben wie die bei
Records. Lediglich durch das Mehrfacherben ergibt sich eine Erweiterung.
Beispiel:
Definition:
DEFINITION MODULE Lists ( Node : POINTER TO NodeObj);
TYPE
NodeObj = OBJECT
prev,next : Node;
END;
List = POINTER TO OBJECT
first,last : Node;
METHOD InsertFirst(n : Node);
METHOD RemoveNode(n : Node);
END;
END Lists;
Ausprägung:
TYPE
TextNode = POINTER TO TextNodeObj;
DEFINITION MODULE TextLists = Lists(TextNode);
TYPE
TextNodeObj = OBJECT OF TextLists.Node;
text : CLASSPTR TO STRING
END;
TextList = TextLists.List;
Mehrfache Ausprägung:
TYPE
Edge = POINTER TO EdgeObj;
DEFINITION MODULE FLists = Lists(Edge.fnode);
DEFINITION MODULE TLists = Lists(Edge.tnode);
TYPE
Vertex = POINTER TO OBJECT OF FLists.List AS from,
TLists.List AS to;
END;
EdgeObj = OBJECT OF FLists.Node AS fnode,
TLIsts.Node AS tnode;
from,to : Vertex;
METHOD Add(from,to : Vertex);
METHOD Remove;
END;
METHOD Edge.Add(from,to : Vertex);
BEGIN
from.from.InsertFirst(SELF);SELF.from:=from;
to.to.InsertFirst(SELF);SELF.to:=to;
END Add;
METHOD Edge.Remove;
BEGIN
from.from.Remove(SELF);from:=NIL;
to.to.Remove(SELF);to:=NIL;
END Remove;
Anhand der generischen Ausprägung der beiden Listenknoten in "Edge" wird
beim Aufruf von "InsertFirst"/"Remove" der richtige der beiden Knoten
automatisch erkannt. Andernfalls müßten die Knoten in der Kante über die
Qualifizierung bezeichnet werden.
7. Resourcetracking mit Objekten
Objekte werden wie alle anderen Speicherstücke auch über Resources
alloziert. Die dazu verwendeten Funktionen sind jedoch gegenüber dem
Programmierer durch Constructoren und Destructoren versteckt. Objekte
existieren relativ zu einem Kontext; bei dessen Vernichtung werden auch sie
vernichtet. Um objektbezogene Resourcen (wie zusätzlichen Speicher oder
Systemelemente) immer mit dem Objekt (also auch bei Freigabe des Objekts
durch Kontextvernichtung) freizugeben, sollten "Destruct"-Methoden verwendet
werden (siehe auch Beispiel in 3.3).
Ein anderes Problem besteht darin, daß Objekte immer zum aktuellen Kontext
erzeugt werden. Dies ist in manchen Fällen nicht wünschenswert. Dafür
besteht die Möglichkeit, den Kontext eines Objektes zu ändern, es also in
einen anderen Existenzbereich umzuhängen. Dies wird vorläufig durch die
Resources Funktion "ChangeObjContext" realisiert.
Beispiel:
TYPE
DosStream = POINTER TO OBJECT OF Stream;
fh : Dos.FileHandlePtr;
CONSTRUCTOR Create(REF name : STRING;con : Context := NIL);
CONSTRUCTOR Open(REF name : STRING;con : Context := NIL);
DESTRUCTOR Close;
METHOD Read(VAR c : CHAR);
METHOD Write(c : CHAR);
METHOD Destruct;
END;
METHOD DosStream.Create(REF name : STRING;con : Context);
BEGIN
IF con#NIL THEN
Resources.ChangeObjContext(SELF,con);
END;
fh:=Dos.Open(name,Dos.newFile)
ASSERT(fh#NIL,Dos.ObjectNotFound);
END Create;
METHOD DosStream.Open(REF name : STRING;con : Context);
BEGIN
IF con#NIL THEN
Resources.ChangeObjContext(SELF,con);
END;
fh:=Dos.Open(name,Dos.oldFile)
ASSERT(fh#NIL,Dos.ObjectNotFound);
END Open;
METHOD DosStream.Close;BEGIN END Close;
Die Funktion "ChangeObjContext" wird, wenn diese Lösung beibehalten wird,
zu einer Standardfunktion erhoben.