Meine Zielsetzung hier war:
- einfache Klassen-Erstellung (inklusive vTables)
- Vererbung (inklusive überschreiben von Methoden der Parents
- Pointer auf Objekte
- Modulunterstützung (Sowohl Objekte und Klassen können Privat und Öffentlich sein
- Konstruktor
- Destruktor
- Kopierkonstruktor
- Speicherverwaltung möglichst durch PureBasic und nicht mittels AllocateMemory()
- Fehlersuche unterstützen
Definition einer Klasse
Eigentlich sehr einfach, zum Beispiel so
Code: Alles auswählen
Class(cName)
ParentClass(cParent) ;optional
DeclareMethods
<Liste der Methoden, wie man sie in Interface angibt>
Properties
<Membervariablen, wie man sie in einer Struktur angibt>
Methods
<Definitionen der Methoden>
EndClass
Auch darf man nichts weglassen. z.b. wenn keine Membervariablen benötigt werden, muss man trotzdem die Properties-Zeile schreiben.
Die Properties sind immer Privat. Leider ist es nicht so einfach, die öffentlich zu machen. Das seh ich aber nicht als so großen Beinbruch, da man die eh am besten per Methoden abfragen kann. Statische Properties sind leider auch nicht möglich. Hier muss man auf Globale Variablen zurück greifen.
die Definitionen der Methoden ist prinzipiell wie Proceduren aufgebaut, hier ein Beispiel:
Code: Alles auswählen
Method(i,Set,Var.x)
*self\Value=Var
self\CheckValue()
MethodeReturn *self\Value
EndMethod
Leider ist ein "Method.i name(" nicht möglich. Der Rückgabetyp muss hier als erster Parameter angeben werden. Die Variablenliste kann aktuell bis zu 10 Parameter lang sein (kann aber erweitert werden. Dazu muss man sich nur das Method-Macro anschauen).
Die Methoden müssen von Rückgabetyp und der Variablenliste mit denen in der Deklaration überein stimmen. Leider ist es nicht möglich zu überprüfen, ob das wirklich so ist.
self
Mit *self\ kann man auf die Properties zugreifen, lesen und schreiben. Mit self\ kann man andere Methoden der Klasse aufrufen.
Das wars schon. Anschließend kann man die Klasse benutzen.
Das einzige was man beachten muss: Die Klassendefinition enthält ausführbaren Code (im Hintergrund wird bspw. eine vTable gefüllt). Deshalb darf man eine Klasse nicht in einer Procedure oder innerhalb If-Endif oder ähnlichen definieren. Ansonsten ist ein Absturz ziemlich sicher.
Spezielle Methoden Initalize, Dispose, Clone
Initalize()
Das ist der Konstruktor. Er wird aufgerufen, sobald ein Objekt erstellt wird. Wenn eine Klasse einen Parent hat, werden alle Initalize()-Methoden sämtlicher Parents ausgeführt. Die Reihenfolge ist dabei fest, erst wird das Parent Initalize() dann das Child.
Ein Initalize muss sich also nur um die Properties kümmern, die neu in dieser Klasse dazugekommen sind.
Dispose()
Der Destruktor, er wird aufgerufen, sobald ein Objekt zerstört werden soll. Genauso wie Initalize werden alle Dispose()-Routinen von Parent und Child aufgerufen. Die Reihenfolge ist hier aber umgekehrt. Erst wird das Child Dispose() dann das Parent.
Nützlich bspw. um Handles freizugeben, die in der Methode geöffnet wurden. Aber auch um mittels Debug Fehler auszugeben, weil man bspw. mit einer Methode eine Datei geöffnet hat, aber später nicht mehr geschlossen hat.
Clone()
Wenn eine Kopie von einen Objekt erstellt wird, erzeugt mein Code eine wirkliche 1:1-Kopie. Das Problem ist, wenn in den Properties bspw. ein Pointer zu einen mit AllocateMemory() angeforderter Speicher vorhanden ist, dann zeigen sowohl Original als auch Kopie auf den exakt den gleichen Speicher. Wenn jetzt das Original den Speicher frei gibt, dann bekommt die Kopie davon nichts mit. Und sobald die Kopie das gleiche versucht crash das Programm.
Die Clone-Methode ist dazu da dieses Problem zu lösen. Und ja, man erhält keinen Parameter mit den Original-Objekt wie bspw in C++.
Das Objekt wurde schon vollständig kopiert (Arrays, Lists und Maps werden dabei korrekt gehandhabt). Man muss nur noch so Sachen wie Pointer, Handles etc. beachten.
Beispiel: In den Properties gibts *buffer und bufferlen, der Clone würde so aussehen:
Code: Alles auswählen
Method(i,Clone)
define *save=*self\buffer
if *self\buffer
*self\buffer=AllocateMemory(*self\bufferlen)
if *self\buffer=0
MethodReturn #false
endif
CopyMemory(*save,*self\buffer,*self\bufferlen)
endif
MethodReturn #true
EndMethod
WICHTIG: Wenn das Clonen fehlschlägt, wird als nächstes Dispose aufgerufen! Man sollte also alle kritischen Properties so setzen, das sie kein Problem machen und das Original-Objekt nicht zerstören können.
Z.b.: Obriges Beispiel, nur das es zwei Speicherbereiche gibt. Die Speicheranforderung schlägt beim ersten fehl, dann muss vor MethodReturn unbedingt der Pointer auf den zweiten Speicherbereich auf Null gesetzt werden (der verweist ja noch auf das Original).
Code: Alles auswählen
Method(i,Clone)
define *save=*self\buffer
if *self\buffer
*self\buffer=AllocateMemory(*self\bufferlen)
if *self\buffer=0
*self\buffer2=0 ; der verweist noch auf das Original!
MethodReturn #false
endif
CopyMemory(*save,*self\buffer,*self\bufferlen)
endif
if *self\buffer2
*save=*self\buffer2
*self\buffer2=AllocateMemory(*self\bufferlen2)
if *self\buffer2=0
MethodReturn #false
endif
CopyMemory(*save,*self\buffer2,*self\bufferlen2)
endif
MethodReturn #true
EndMethod
PureBasic hasst Dispose
Mit PureBasic gibt es leider ein Problem. Wann ein Objekt Initalize aufrufen soll, ist einfach, aber es gibt keine automatische Möglichkeit ein Dispose aufzurufen.
Ein lokales Objekt ist bis zum "End" gültig, oder in einer Procedure bis EndProcedure oder ProcedureReturn.
Aus diesen Grund muss man diese Steuerworte durch _End , _EndProcedure und _ProcedureReturn ersetzen, sobald man Objekte benutzt. Ein Fehlendes _EndProcedure erkennen die Macros normalerweise selbstständig und verhindern ein Compilen. Ein fehlendes _End merkt man daran, das in Debug-Fenster ein "[INFO] _end" nicht erscheint. Leider gibt es keine Möglichkeit, ein fehlendes _ProcedureReturn festzustellen.
Aber wenn der Debugger läuft, erhöht jedes lokal erstellte Objekt einen Zähler und wenn ein Objekt korrekt mit Dispose zerstört wird, verringert sich der Zähler. Wenn dieser Zähler bei _End nicht Null ist, wird eine entsprechende Meldung ausgegeben!
"[WARNING] not disposed objects: ". Also es ist möglich festzustellen ob ein Fehler passiert ist.
Wie Schlimm ist ein fehlendes _ProcedureReturn?
Schwer zu sagen. Lokale Objekte werden vollständig in lokalen Variablen gespeichert. Diese gibt dann PureBasic frei. Sämtliche Handles, Speicher und ähnliches, die durch das Objekt angefordert wurden und normalerweise durch Dispose wieder freigegeben werden könnten, bleiben bis zum Programmende offen.
_End
_End funktioniert leider nicht in Proceduren! Allerdings sollte man End eh nicht in Proceduren aufrufen, außer man stellt fest, das ein gröberer Fehler passiert ist und man lieber den Stecker ziehen sollte.
_FakeEnd
Verhält sich genau wie _End nur dass das Programm nicht beendet wird. Allerdings sind sämtliche Objekte, also auch statische und globale, zerstört und eine Verwendung führt zu einem Absturz. _FakeEnd ist mehr für Debug-Zwecke gedacht.
Objekt-Vollüberwachung aktivieren
Wenn man vor den Include der Module_oop.pbi folgende Konstante setzt:
Code: Alles auswählen
#__class_debug_all_objects=#True
XIncludeFile("Module_oop.pbi")
So kann man fehlende _ProcedureReturns rausfinden. Nachteil ist, das die Objekt-Erstellung und Zerstörung langsamer wird und Speicher für die Protokollierung drauf geht.
Die Routinen sind nur aktiv, wenn ein Debugger an ist (wie sollte sonst auch die Ausgabe erfolgen?).
Objekte erstellen
Define_Object(<object name>,<class name>)
Protected_Object(<object name>,<class name>)
Global_Object(<object name>,<class name>)
Static_Object(<object name>,<class name>)
Einfach eines dieser Makros aufrufen und das Objekt wird definiert und initialisiert. Eins ist nur wichtig, dahinter befindet sich immer Code. Sowas
Code: Alles auswählen
If #False
Define_Object(MyObj,cMyClass)
EndIf
Die Objekte werden übrigens vollständig in lokalen/globale/statischen Variablen erstellt. Zerstört werden sie automatisch bei _EndProcedure, _ProcedureReturn und _End.
Pointer
Erzeugt man wie jeden anderen Pointer auch.
Code: Alles auswählen
*<pointer name>.<class name>
ACHTUNG, aufgrund wie PureBasic Interface handhabt, sollte man das hier beachten
Code: Alles auswählen
Define_Object(obj1,cMyClass)
Define_Object(obj2,cMyClass)
Define *pObj.cMyClass
obj1=obj2 ;Bad
*pObj=@obj1 ; Won't work
*pObj=obj1 ; Work
*pObj1=@obj1 funktioniert auch nicht. Grund ist, das obj1 für PureBasic prinzipiell ein Pointer ist und man so die Adresse kriegt, die Adresse von Object enthält, also einen doppelten Pointer.
Allocate_Object(<class name>)
Free_Object(<object>)
Mittels diesen Funktionen kann man auch Objekte erzeugen. Diese werden aber mit "AllocateStructure" erzeugt und bleiben auch bestehen, wenn bspw. eine Procedure endet. Man muss sie manuell mit Free_Object() wieder freigeben. Das ist die einzige Möglichkeit, wie man bspw. Objekte in einer List, Map oder innerhalb einer Struktur unterbringen kann. Einfach einen Pointer dort erzeugen und mit Allocate_Object ein Objekt zuweisen.
Wichtig: _End kann diese Objekte nicht freigeben! Es gibt nur aus, ob solche Objekte nicht korrekt freigegeben wurden ("[WARNING] not disposed allocated objects: ").
Vererbung und Methoden überschreiben
Ein Beispiel sagt mehr als tausend Worte:
Code: Alles auswählen
Class(cVar)
DeclareMethods
Set(x)
Get()
Properties
value.i
Methods
Method(i,Set,x.i)
*self\value=x
EndMethod
Method(i,Get)
MethodReturn *self\value
EndMethod
EndClass
Class(cVar2)
ParentClass(cVar)
DeclareMethods
OldSet(x)
Properties
Methods
AliasMethod(Set,OldSet)
Method(i,Set,x)
*self\value=x*2
EndMethod
EndClass
Define_Object(obj1,cVar2)
Define *obj2.cVar
obj1\Set(20)
Debug obj1\Get(); Return 40
obj1\OldSet(20)
Debug obj1\Get(); Return 20
*obj2=obj1
*obj2\Set(30)
Debug obj2\Get(30); Return 60!
Mittels AliasMethod wird die ursprüngliche Set-Methode von cVar gerettet. Das muss vor den überschreiben der Methode passieren. Anschließend kann man die ursprüngliche Set()-Methode mit OldSet() aufrufen. Der Alias muss vorher in DeclareMethods deklariert werden.
Wichtig hier ist, das sich die Anzahl der Parameter nicht ändern darf. Es wird zwar keine Überprüfung durchgeführt (weil nicht möglich), aber der Versuch wird vermutlich die lustigsten Effekte verursachen und man wird ewig nach den Fehler suchen.
Vielleicht wird es einige Überraschen, warum der Pointer, der eigentlich von Typ cVar ist, bei Set die überschriebene Version von cVar2 aufruft. Grund ist, dass das Objekt selbst bestimmt, welche Funktionen aufgerufen werden und das Objekt ist von Typ cVar2.
Es ist völlig ungefährlich, ein Child (hier Typ cVar2) einen Parent (cVar) zuzuweisen, weil in Child alle Methoden und Properties von Parent vorhanden sind.
Eine sichere Weise, ein Objekt zuzuweisen
PureBasic unterstützt leider überhaupt keine Typen-Überprüfung. Es ist also durchaus möglich, das man einen Pointer ein Objekt zuweist, das überhaupt nicht zum Pointer passt. Das Ergebnis dürfte mehr als undefiniert sein, weil irgendwelche Methoden mit unsinnigen Parametern von Objekt aufgerufen wird, wenn man nach so einer Fehlzuweisung versucht darauf zuzugreifen.
Darum gibt es Funktionen, eine Klasse zu verifizieren:
Code: Alles auswählen
*obj2=Object_CheckClass(obj1,cVar)
Wenn also eine Zuweisung "fehlschlägt" erhält man ein Null-Objekt und wenn man darauf zugreift, stürzt das Programm immer "sauber" ab. Und das ist bedeutend besser, als wenn das Programm willkürlich irgendwelche Methoden aufruft.
Für Proceduren, die drauf angewiesen sind, einen Pointer von einen Objekt zu erhalten, können diesen auch mit folgender Funktion überprüfen:
Object_ForceClass(<object>,<class name>)
Wenn das Objekt zur Klasse nicht passt, wird in Debugger eine Fehlermeldung ausgegeben und das Programm wird angehalten. Ohne Debugger wird das ganze sogar sofort beendet. Die Idee dahinter ist, das irgendwas komplett schief gelaufen ist und ein weiteres Ausführen des Programms nur in eine größere Katastrophe führt.
Spezielle Klasse oop::object
oop::object ist quasi die Root-Klasse. Aus dieser werden sämtliche andere Klassen abgeleitet. Ein Pointer diesen Typs kann also sämtliche Objekte gefahrlos handhaben.
Diese Klasse enthält auch einige Basis-Methoden, die alle Klassen beherrschen.
Methode GetClassName()
Gibt die Klasse des Objekt als String zurück (nicht die Klasse des Pointers!).
Methode Size()
Gibt den Speicherverbrauch des Objekts zurück. Leider etwas unvollständig, wenn Arrays, Listen, Maps in den Properties vorhanden sind, werden die nicht berücksichtigt. Genauso wenn man selbst Speicher anfordert und in einen Pointer speichert.
Methode CloneFrom(<source-object>)
Zerstört das Objekt und erstellt anschließend eine Kopie von Source-Objekt.
Hier ein Beispiel:
Code: Alles auswählen
Protected_Object(obj1,cVar)
Protected_Object(obj2,cVar)
obj2\Set(30)
obj1\CloneFrom(obj2)
Sollte das Clonen fehlschlagen, gibt CloneFrom() #False zurück, ansonsten #True.
Methode AllocateClone()
Erstellt einen Clone von Objekt und gibt ihn zurück. Dieser Clone muss manuell mit Free_Object() wieder freigegeben werden. Sollte das Objekt nicht geclont werden können, wird #Null zurückgegeben.
Reset()
Zerstört das Objekt und initialisiert es neu.
Anwendungsmöglichkeiten
Mit der Basisklasse oop::object kann man Objekte Clonen, den Typ abfragen und den Klassennamen herausbekommen, ohne dass man weis, was für ein Objekt man hier hat.
Man kann bspw. eine Procedure schreiben, die verschiedene Objekte entgegennehmen kann und anschließend verzweigt. z.b.
Code: Alles auswählen
;Class cText is used for storing long text.
;Class cPicture is used for storing pictures.
Procedure Print_Objekt(*obj.oop::object)
Define *text.cText
Define *pic.cPicture
If Object_Class(*obj,cText)
*text=*obj
<print *text>
ElseIf Object_Class(*obj,cPicture)
*pic=*obj
<print *picture>
EndIf
EndProcedure
Wie versprochen, meine Routinen können in Modulen verwendet werden. Dazu muss aber mittels
Code: Alles auswählen
UseModule EnableClass
Lokale und öffentliche Objekte
Hier ändert sich eigentlich überhaupt nichts. Einfach wie bisher mit bspw. Define_Object() das Objekt entweder in DeclareModule-EndDeclareModule oder Modul-EndModule - Abschnitt definieren. Wie man es bisher auch mit Variablen gemacht hat.
Lokale Klassen
Auch das hier ist einfach, einfach die Klasse in Module-EndModule unterbringen und sie kann anschließend in Modul benutzt werden.
Öffentliche Klassen
Hier wird es etwas komplizierter. Man muss die Klasse dann in DeclareModule-EndDeclareModule deklarieren und anschließend in Module-EndModule definieren.
Dafür gibt es die neuen Steuerwörter DeclareClass-EndDeclareClass und enthält die Steuerwörter "ParentClass"(falls vorhanden) "Declare Methods" und "Properties". DefineClass-EndDefineClass enthält dann nur noch die Methoden.
Ein Beispiel sagt mehr als tausend Worte:
Code: Alles auswählen
DeclareModule TestModul1
UseModule EnableClass
DeclareClass(cTM1)
DeclareMethods
Get()
Set(v.i)
Properties
value.i
EndDeclareClass
EndDeclareModule
Module TestModul1
DefineClass(cTM1)
Method(i,Get)
MethodReturn *self\value
EndMethod
Method(i,Set,v.i)
*self\value=v
EndMethod
EndDefineClass
EndModule
Objekte in Klassen
Sind jetzt auch möglich. Leider ist es nicht ganz so einfach. Man muss in den Properties das Objekt definieren und in der Methode Initalize initialisieren. Um Clone und Dispose muss man sich nicht kümmern, das macht mein Code automatisch
Declare_Object(<object-name>,<class>[,<arraysize>)
Kann nur in Properties-Bereich benutzt werden. Arraysize ist optional. Zukünftig kann man dann mit bspw. *self\<objectname>[1] darauf zurückgreifen.
Initalize_Object(<object-name>,<class>[,<arraysize>)
Muss innerhalb der Initalize-Methode benutzt werden. Die Angaben müssen mit Declare übereinstimmen.
Hier ein Beispiel:
Code: Alles auswählen
Class(cDeep2);- cDeep2
DeclareMethods
Properties
Declare_Object(var,cDeep1,2)
Methods
Method(i,Initalize)
Initalize_Object(var,cDeep1,2)
*self\fakemem=fakealloc()
EndMethod
EndClass
SizeOf_Class(<class name>)
Gibt die Größe eines Objekt der Klasse zurück.
DebugCreate_Obj(<obj name>,<message>)
Gibt eine Message in Debug aus und wann und wo das Objekt erstellt wurde. Die Funktion verhält sich wie der Debug-Befehl.
Kann bspw. praktisch sein, wenn man in Dispose() feststellt, das Handles nicht korrekt geschlossen wurden.
Internes
Da ich die Speicherverwaltung möglichst ohne AllocateMemory durchführen wollte, erzeugen die Macros einige zusätzlich Variablen,Labels,Strukturen. Normalerweise braucht man sich nicht darum kümmern.
Was wird alles bei der Klassen-Definition
Wenn eine Klasse "MyClass" erstellt wird, passiert in Hintergrund folgendes:
Class sichert den Klassenname in der DataSection und markiert die Stelle mit einem Label "MyClass__Class__Name" versehen.
DeclareMethods ist eigentlich eine Interface-Erstellung mit "MyClass" als Interface-Namen.
Properties erzeugt eine Struktur "MyClass__Class__struc", in der sämtliche Properties und auch einige interne Variablen wie der Pointer zur vTable gespeichert werden.
Methods erzeugt eine Struktur "MyClass__Class__VTable" und eine globale Variable "MyClass__Class__functions.MyClass__Class__VTable". Sämtliche Objekte dieser Klasse benutzen genau diese eine vTable.
Method-EndMethode ist eigentlich ein Procedure-EndProcedure-Aufruf. Die Procedure wird allerdings in "MyClass__Class__<Methoden-name>" umbenannt. Am Ende wird die so erstellte Procedure in der VTable mittels PokeI() eingetragen.
EndClass erstellt noch zwei Proceduren ("MyClass__Class____CopyStructure" und "MyClass__Class____AllocateStructure") und trägt sie in die vTable ein. Die beiden Routinen werden intern für Clone benötigt.
Was wird alles bei der Objekt-Definition
Define_Object()
Define_Object(MyObj,MyClass) macht folgendes:
Falls noch nicht passiert, wird eine lokale Variable "*__Class__define_dispose_chain" erstellt. Diese Variable enthält immer das zuletzt erstellte Objekt und wird für Dispose benötigt.
Als erstes wird das eigentliche Objekt mit "Define MyObj__Class__obj.MyClass__Class__struc" erstellt und die Variable "Define MyObj.MyClass" und das Objekt initalisiert.
Protect_Object(), Static_Object() und Global_Object verlaufen analog dazu.
Allocate_Object()
Erzeugt keinerlei zusätliche Sachen.
Was ist die Dispose-Chain?
Um lokale Objekte frei zu geben, müssen sie irgendwo eingetragen werden. Ich nutzte dazu die oben genannte Variable. Wenn ein neues Objekt erstellt wird, wird der Wert der Variable in Objekt gespeichert und anschließend wird die *__Class__define_dispose_chain auf das neue Objekt gesetzt. Auf diese Weise kann ich bei einen _EndProcedure oder _ProcedureReturn die Kette wieder aufzwirbeln und jedes Objekt auflösen. Die Clone-Funktionen sind so geschrieben, das sie die Kette nicht zerstören.
Globale Objekte (und statische) bauen eine eigene Dispose-Chain auf und werden bei "_End" freigeben.
AllocateObjekt() werden in diesen Chains nicht erfasst!
Warum zwei Module?
Das Modul EnableClass muss ja alle mittels "UseModul" überall freigegeben werden, sonst funktionieren die ganzen Makros nicht mehr. Ich brauch aber eine Möglichkeit, verschiedene Variablen, Macros und Proceduren unterzubringen, die von überall aufgerufen werden sollen. Darum das Modul oop.
Editoreinstellungen
Ich empfehle unter Costum-Keywords folgende Wörter hinzuzufügen:
Code: Alles auswählen
Class
DeclareClass
DefineClass
ParentClass
DeclareMethods
Properties
Methods
EndDeclareClass
Method
MethodReturn
EndMethod
AliasMethod
EndClass
EndDefineClass
Allocate_Object
Free_Object
Protected_Object
Global_Object
Define_Object
Static_Object
Declare_Object
Initalize_Object
_FakeEnd
_End
_EndProcedure
_ProcedureReturn
self
Code: Alles auswählen
_EndProcedure -1 0
Class 0 1
DeclareClass 0 1
DefineClass 0 1
DeclareMethods 0 1
EndClass -2 0
EndDeclareClass -2 0
EndDefineClass -1 0
EndMethod -1 0
Method 0 1
Methods -1 1
Properties -1 1
Das Ende
Wie immer, über feedback würde ich mich freuen. Es ist schon erstaunlich, wieviel OOP eigentlich schon in PureBasic steckt und man nur aus Designgründen darauf verzichtet, damit die Sprache "sauber" bleibt. Es muss ja nicht gleich ein Machwerk wie bei C++ sein.