Module_Class - Klassen mit "IncludeFile" und ohne Verwaltung
Verfasst: 05.01.2017 13:53
Klassen in PB sind irgendwie mein Lieblingsthema geworden. Leider waren meine Lösungen meist recht kompliziert (ok, streng genommen, die hier auch ein bischen) und benötigen Verwaltungsaufwand durch das Programm. Kompliziert wirds dann noch wenn mit Vererbung und Objekte in Objekten gearbeitet wird. Da kommen dann zum schluss gegiantische Konstrukte mit Goto und ähnlichen raus.
Ich denke ich hab jetzt eine gute Lösung, die mit keinerlei Verwaltungsaufwand (die VTable wird in einen Data-Block abgelegt) seitens des Programm kommt und auch Objekte als Members akzeptiert, ohne das es Probleme gibt. Zudem kann man Klassen in Modulen definieren. Eine kleine Einschränkung gibt es allerdings doch: Parent und Child müssen zum selben Modul gehören!
Wie man eine Klasse deklariert:
Die Parameter von Method sind Rückgabetyp,Funktionsname,Parameterliste. Member werden analog definiert (Typ,Name).
Member-Variablen sind durch die Methodik von PB immer geschützt, man kann auf sie zwar direkt Zugreifen, das ist aber immer mit ein bischen aufwand verbunden ( *self.sres=obj)
Nach dem Aufruf von class::declare(res) ist die Klasse schon einsatzbereit.
Die Klasse wird immer mit <klassenname>_create() erzeugt und wird wird mit <objektname>\dispose() vernichtet.
Jetzt müssen nur noch alle Member definieren:
Hier gibts es ein paar Besonderheiten:
class::Define(res) muss als letztes ausgeführt werden. Er erstellt dann die vtable und die Haupt-Konstruktoren/Destruktoren.
Die Proceduren müssen immer <Klasse>_<Member> heißen und der Pointer *this.s<klasse> muss immer als erster Parameter vorhanden sein. Er enthält das Objekt selbst. Wenn man eine andere Methode der Klasse aufrufen will, kann man das einfach durch *this\self\<method>() aufrufen. *this\self zeigt Quasi auf sich selbst. man könnte statt *this\self\get() auch res_get(*this) schreiben. Nur, wenn das Objekt ein Child von res ist, kann es sein, dass das Child den Member Get umdefiniert hat. Will man sicherstellen, das die aktuelle Methode aufgerufen wird, sollte man immer *this\self\get() nutzen, wenn man zwingend die eigene Routine nutzen muss, res_get(*this).
Es gibt zwei besondere Methoden <klasse>_new(*this) und <klasse>_dispose(*this).
_new() ist der Konstruktor. Er wird aufgerufen, wenn der Speicher reserviert wurde und alles andere Initalisiert wurde (bspw. Parent-Klassen, Objekte in Objekte). Er muss als Rückgabewert sich selbst zurückgeben! Er kann genutzt werden, um Member-Variablen mit Defaultwerten zu füllen. Da die Parent-Konstruktoren schon aufgerufen sind, braucht er sich um vererbte Member nicht kümmern.
_dispose() ist der Destruktor. Er wird aufgerufen, bevor der Speicher freigeben wird, bevor die Parent-Klassen Destruktoren aufgerufen und bevor Objekte in Objekte gelöscht werden. Auch hier gilt, das er sich nicht um Parent-Member kümmern muss, das macht der Parent-Destruktor.
Die beiden Methoden dürfen nicht oben deklariert werden und sind optional. Genauso kann ein Parent einen Destruktor haben und das Child nicht und umgekehrt.
Objekte in Objekten werden mit mit Object(typ,name) erstellt:
Vererbungen sind auch möglich. Hier wird Klasse res3 mit Parent Res2 erstellt:
Wenn man den Code ausführt, sollte man bemerken, das beide Konstruktoren aufgerufen werden.
Bei den Beispiel wird zudem die Methode "get" von res2 durch eine Methode von res3 ersetzt.
Natürlich kann man noch eine eben weiter vererben und man kann Maps, Listen, und Felder benutzen. Bei den letzten drei kann man aber nicht direkt Objekte nutzen, aber Pointer auf Objekte. Die müsste man in new() füllen und dispose() löschen.
schauen wir uns mal an, wie der Code von res4 nach Aufruf meiner klassenfunktionen aussieht (geht leicht hiermit: http://www.purebasic.fr/german/viewtopi ... preprocess )
Besonderheiten hier: Obwohl die Klasse res4 ja mittels extends von res3 (die wiederum von res2) erweitert wurde, wird in Interface und Structure nichts dergleichen gemacht. Beim Interface wäre es möglich gewesen, bei der Structure nicht. Der zweite Eintrag "*self.res4" wäre sonst nicht möglich. Der Einfachhalts halber nutze ich bei beiden immer die komplette Liste.
*_value.res wird übrigens in res2 angelegt und initialisiert, der Haupt-Konstruktor (_create) hangelt sich da durch.
Die Procedure "res4_free_" ist der Haupt-Destruktor. Er wird eigentlich aufgerufen, wenn man <object>\dispose() aufruft. Genauso wie beim Konstruktor hangelt er sich durch alle Parents hindurch.
so und hier die nötigen Dateien:
http://game.gpihome.eu/PureBasic/objtest2.7z
Ein paar Tricks, die ich hier anwende:
Neben XIncludeFile gibt es ja noch IncludeFile, wo man die gleiche Datei immer und immer wieder einfügen kann. Klingt banal, ist aber hier extrem wichtig. Ich nutze die Macros dazu, eben die hinzugefügte Datei umfassend zu ändern. Gerade weil man hier ungestört Macros erstellen kann.
Es gibt ja die Möglichkeit mittels Macros Macros in Macros zu erzeugen. Dabei hab ich festegestellt, das diese auch Zeilenumgreifen funktionieren, obwohl das eigentlich nicht möglich sein solle.
Gerade das ifthen-Macro ist unglaublich praktisch. Damit kann Macros von Child zum Parent erzeugen, die nachschauen, ob in der Klasse die Member definiert sind oder in der Parentklasse. Keine Ahnung, wie weit man schachteln kann, bevor der Compiler in die Knie geht. Zweimal geht schon mal ohne Probleme 
Und funfact - wenn man in der "module_class_declaraion.pbi" die Reihenfolge der am Anfang leicht ändert von
nach
kann man die test.pb einmal korrekt kompilen und starten, beim zweiten mal (ohne Änderung) schmeißt der Editor/Compiler völlig unsinnige Fehlermeldungen
Module_class_declaration.pbi - Line 5: Data can only be declared in a Datasection.
Und das hier in Macro Fenster:
Keine Ahnung, was da schief läuft, ist ein Bug von PB
Wer gerne die Autovervollständigung nutzen will, kann ja mittels meiner module_class und http://www.purebasic.fr/german/viewtopi ... preprocess eine macrofreie Version der Klassen erzeugen. Also erstmal alle Klassen in einer Datei, PreProcess aufrufen und dann diese in den eigenen Programmen nutzen. Die "test.pb.pre.pb" in der 7z wurde so erzeugt. Die Reste des Module "class" kann man mehr löschen, da taucht eh nur eine Konstante auf, die nirgends mehr gebraucht wird. Ist auch ganz praktisch, wenn man sehen will, wie das ganze Arbeitet.
Ich denke ich hab jetzt eine gute Lösung, die mit keinerlei Verwaltungsaufwand (die VTable wird in einen Data-Block abgelegt) seitens des Programm kommt und auch Objekte als Members akzeptiert, ohne das es Probleme gibt. Zudem kann man Klassen in Modulen definieren. Eine kleine Einschränkung gibt es allerdings doch: Parent und Child müssen zum selben Modul gehören!
Wie man eine Klasse deklariert:
Code: Alles auswählen
Macro DeclareClass_res()
Method(i,set,(a.i=0))
Method(i,add,(b.i))
Method(i,get,())
Member(i,_value)
EndMacro
class::Declare(res)
Member-Variablen sind durch die Methodik von PB immer geschützt, man kann auf sie zwar direkt Zugreifen, das ist aber immer mit ein bischen aufwand verbunden ( *self.sres=obj)
Nach dem Aufruf von class::declare(res) ist die Klasse schon einsatzbereit.
Die Klasse wird immer mit <klassenname>_create() erzeugt und wird wird mit <objektname>\dispose() vernichtet.
Code: Alles auswählen
obj.res=res_create()
Debug obj\get()
obj\set(10)
Debug obj\get()
obj\add(20)
Debug obj\get()
obj=obj\dispose()
Debug "----"
Code: Alles auswählen
Procedure.i res_set(*this.sres,value.i=0)
Debug "res_set"
*this\_value=value
ProcedureReturn *this\_value
EndProcedure
Procedure.i res_add(*this.sres,value.i)
Debug "res_add"
;this\_value+value
*this\self\set(*this\self\get()+value)
ProcedureReturn *this\_value
EndProcedure
Procedure.i res_get(*this.sres)
Debug "res_get"
ProcedureReturn *this\_value
EndProcedure
Procedure res_new(*this.sres)
Debug "konstruktor res"
ProcedureReturn *this
EndProcedure
Procedure res_dispose(*this.sres)
Debug "destruktor res"
EndProcedure
class::Define(res)
class::Define(res) muss als letztes ausgeführt werden. Er erstellt dann die vtable und die Haupt-Konstruktoren/Destruktoren.
Die Proceduren müssen immer <Klasse>_<Member> heißen und der Pointer *this.s<klasse> muss immer als erster Parameter vorhanden sein. Er enthält das Objekt selbst. Wenn man eine andere Methode der Klasse aufrufen will, kann man das einfach durch *this\self\<method>() aufrufen. *this\self zeigt Quasi auf sich selbst. man könnte statt *this\self\get() auch res_get(*this) schreiben. Nur, wenn das Objekt ein Child von res ist, kann es sein, dass das Child den Member Get umdefiniert hat. Will man sicherstellen, das die aktuelle Methode aufgerufen wird, sollte man immer *this\self\get() nutzen, wenn man zwingend die eigene Routine nutzen muss, res_get(*this).
Es gibt zwei besondere Methoden <klasse>_new(*this) und <klasse>_dispose(*this).
_new() ist der Konstruktor. Er wird aufgerufen, wenn der Speicher reserviert wurde und alles andere Initalisiert wurde (bspw. Parent-Klassen, Objekte in Objekte). Er muss als Rückgabewert sich selbst zurückgeben! Er kann genutzt werden, um Member-Variablen mit Defaultwerten zu füllen. Da die Parent-Konstruktoren schon aufgerufen sind, braucht er sich um vererbte Member nicht kümmern.
_dispose() ist der Destruktor. Er wird aufgerufen, bevor der Speicher freigeben wird, bevor die Parent-Klassen Destruktoren aufgerufen und bevor Objekte in Objekte gelöscht werden. Auch hier gilt, das er sich nicht um Parent-Member kümmern muss, das macht der Parent-Destruktor.
Die beiden Methoden dürfen nicht oben deklariert werden und sind optional. Genauso kann ein Parent einen Destruktor haben und das Child nicht und umgekehrt.
Objekte in Objekten werden mit mit Object(typ,name) erstellt:
Code: Alles auswählen
Macro DeclareClass_res2()
Method(i,set,(a.i=0))
Method(i,add,(b.i))
Method(i,get,())
object(res,_value)
EndMacro
class::Declare(res2)
Procedure.i res2_set(*this.sres2,value.i=0)
Debug "res2_set"
ProcedureReturn *this\_value\set(value)
EndProcedure
Procedure.i res2_add(*this.sres2,value.i)
Debug "res2_add"
ProcedureReturn *this\_value\add(value)
EndProcedure
Procedure.i res2_get(*this.sres2)
Debug "res2_get"
ProcedureReturn *this\_value\get()
EndProcedure
Procedure res2_new(*this.sres2)
Debug "konstruktor res2"
ProcedureReturn *this
EndProcedure
Procedure res2_dispose(*this.sres2)
Debug "destruktor res2"
EndProcedure
class::Define(res2)
obj2.res2=res2_create()
Debug"-"
Debug obj2\get()
Debug"-"
obj2\set(10)
Debug"-"
Debug obj2\get()
Debug"-"
obj2\add(20)
Debug"-"
Debug obj2\get()
Debug"-"
obj2=obj2\dispose()
Debug "----"
Code: Alles auswählen
Macro DeclareClass_res3_extends_res2()
method(i,dummy,())
EndMacro
class::Declare(res3,res2)
Procedure res3_dummy(*this)
Debug "dummy"
EndProcedure
Procedure res3_new(*this)
Debug "konstructor3"
ProcedureReturn *this
EndProcedure
Procedure res3_add(*this.sres3,value)
Debug "res3_add"
ProcedureReturn *this\_value\add(value*2)
EndProcedure
class::Define(res3)
obj3.res3=res3_create()
Debug"-"
Debug obj3\get()
Debug"-"
obj3\set(10)
Debug"-"
Debug obj3\get()
Debug"-"
obj3\add(20)
Debug"-"
Debug obj3\get()
Debug"-"
Debug obj3\dummy()
obj3=obj3\dispose()
Bei den Beispiel wird zudem die Methode "get" von res2 durch eine Methode von res3 ersetzt.
Natürlich kann man noch eine eben weiter vererben und man kann Maps, Listen, und Felder benutzen. Bei den letzten drei kann man aber nicht direkt Objekte nutzen, aber Pointer auf Objekte. Die müsste man in new() füllen und dispose() löschen.
Code: Alles auswählen
Debug "-----------------"
Macro DeclareClass_Res4_extends_Res3()
method(s,setmap,(k$,v$))
method(s,getmap,(k$))
member(i,feld,[10])
member(i,List liste,())
member(s,Map karte,())
EndMacro
class::Declare(res4,res3)
Procedure.s res4_setmap(*this.sres4,k$,v$)
*this\karte(k$)=v$
ProcedureReturn *this\karte(k$)
EndProcedure
Procedure.s res4_getmap(*this.sres4,k$)
ProcedureReturn *this\karte(k$)
EndProcedure
Procedure.i res4_get(*this.sres4)
ProcedureReturn *this\feld[3]
EndProcedure
Procedure.i res4_set(*this.sres4,value.i)
*this\feld[3]=value
ProcedureReturn *this\feld[3]
EndProcedure
Procedure.i res4_new(*this.sres4)
Debug "konstrucutro res4"
ProcedureReturn *this
EndProcedure
class::Define(res4)
*obj4.res4=res4_create()
*obj4\setmap("hallo","duda")
*obj4\setmap("haha","gaga")
Debug *obj4\getmap("hallo")
Debug *obj4\getmap("haha")
Debug *obj4\getmap("tusch")
*obj4\set(123)
Debug *obj4\get()
*obj4\dispose()
Code: Alles auswählen
Interface res4
dispose(force=#True)
set.i (a.i=0)
add.i (b.i)
get.i ()
dummy.i ()
setmap.s (k$,v$)
getmap.s (k$)
EndInterface
Structure sres4
*__vtable
*self.res4
*_value.res
feld.i [10]
List liste.i ()
Map karte.s ()
EndStructure
Procedure.s res4_setmap(*this.sres4,k$,v$)
*this\karte(k$)=v$
ProcedureReturn *this\karte(k$)
EndProcedure
Procedure.s res4_getmap(*this.sres4,k$)
ProcedureReturn *this\karte(k$)
EndProcedure
Procedure.i res4_get(*this.sres4)
ProcedureReturn *this\feld[3]
EndProcedure
Procedure.i res4_set(*this.sres4,value.i)
*this\feld[3]=value
ProcedureReturn *this\feld[3]
EndProcedure
Procedure.i res4_new(*this.sres4)
Debug "konstrucutro res4"
ProcedureReturn *this
EndProcedure
Declare res4_free_ (*self,force=#True)
Procedure res4_create ( *self . sres4 =0)
If *self=0
*self=AllocateStructure(sres4)
EndIf
If *self And *self\__vtable=0
*self\__vtable=?res4_vtable
*self\self=*self
EndIf
If *self
*self=res3_create(*self)
EndIf
If *self
ProcedureReturn res4_new (*self)
EndIf
ProcedureReturn 0
DataSection
res4_vtable:
Data.i @res4_free_ ()
Data.i @res4_set ()
Data.i @res3_add ()
Data.i @res4_get ()
Data.i @res3_dummy ()
Data.i @res4_setmap ()
Data.i @res4_getmap ()
EndDataSection
EndProcedure
Procedure res4_free_ ( *self . sres4 ,force=#True)
res3_free_(*self,#False)
If force
FreeStructure(*self)
EndIf
ProcedureReturn 0
EndProcedure
*_value.res wird übrigens in res2 angelegt und initialisiert, der Haupt-Konstruktor (_create) hangelt sich da durch.
Die Procedure "res4_free_" ist der Haupt-Destruktor. Er wird eigentlich aufgerufen, wenn man <object>\dispose() aufruft. Genauso wie beim Konstruktor hangelt er sich durch alle Parents hindurch.
so und hier die nötigen Dateien:
http://game.gpihome.eu/PureBasic/objtest2.7z
Ein paar Tricks, die ich hier anwende:
Neben XIncludeFile gibt es ja noch IncludeFile, wo man die gleiche Datei immer und immer wieder einfügen kann. Klingt banal, ist aber hier extrem wichtig. Ich nutze die Macros dazu, eben die hinzugefügte Datei umfassend zu ändern. Gerade weil man hier ungestört Macros erstellen kann.
Es gibt ja die Möglichkeit mittels Macros Macros in Macros zu erzeugen. Dabei hab ich festegestellt, das diese auch Zeilenumgreifen funktionieren, obwohl das eigentlich nicht möglich sein solle.
Code: Alles auswählen
Macro JoinMacroParts (P1, P2=, P3=, P4=, P5=, P6=, P7=, P8=) : P1#P2#P3#P4#P5#P6#P7#P8 : EndMacro
Macro CreateMacro (name,macroBody=)
class::JoinMacroParts (Macro name, class::MacroColon, macroBody, class::MacroColon, EndMacro) :
EndMacro
Macro CreateQuote (name)
class::JoinMacroParts (class::MacroQuote,name,class::MacroQuote)
EndMacro
Macro CreateSingleQuote (name)
class::JoinMacroParts (class::MacroSingleQuote,name,class::MacroSingleQuote)
EndMacro
;class_extendsvtable
Macro combinelist(name,name2,name3)
class::CreateMacro(name (), name2()
name3())
EndMacro
Macro ifthen(name,class,b,elsemacro)
class::CreateMacro( name (a), CompilerIf Defined(class#_#b,#PB_Procedure)
Data.i @class#_#b ()
CompilerElse
elsemacro (a)
CompilerEndIf)
EndMacro

Und funfact - wenn man in der "module_class_declaraion.pbi" die Reihenfolge der am Anfang leicht ändert von
Code: Alles auswählen
CompilerIf #PB_Compiler_IsMainFile
CompilerError "don't compile me"
CompilerEndIf
class::CreateMacro( __class_extends_#class() , extends() ) :
CompilerIf class::CreateQuote( extends() )<>"$nil"
class::combinelist( __class_declare_#class() ,__class_declare_#extends() , DeclareClass_#class()_extends_#extends() ) :
class::combinelist( __class_current_declare ,__class_declare_#extends() , DeclareClass_#class()_extends_#extends() ) :
CompilerElse
class::combinelist( __class_declare_#class() , DeclareClass_#class() , class::macronil ) :
class::combinelist( __class_current_declare , DeclareClass_#class() , class::macronil ) :
CompilerEndIf
Code: Alles auswählen
CompilerIf #PB_Compiler_IsMainFile
CompilerError "don't compile me"
CompilerEndIf
CompilerIf class::CreateQuote( extends() )<>"$nil"
class::combinelist( __class_declare_#class() ,__class_declare_#extends() , DeclareClass_#class()_extends_#extends() ) :
class::combinelist( __class_current_declare ,__class_declare_#extends() , DeclareClass_#class()_extends_#extends() ) :
CompilerElse
class::combinelist( __class_declare_#class() , DeclareClass_#class() , class::macronil ) :
class::combinelist( __class_current_declare , DeclareClass_#class() , class::macronil ) :
CompilerEndIf
class::CreateMacro( __class_extends_#class() , extends() ) :
Module_class_declaration.pbi - Line 5: Data can only be declared in a Datasection.
Und das hier in Macro Fenster:
Code: Alles auswählen
Macro Extends(): $nil: EndMacro : :
Macro class(): res: EndMacro : :
IncludeFile(class: : #class_includepath+"module_class_declaration.pbi")
Wer gerne die Autovervollständigung nutzen will, kann ja mittels meiner module_class und http://www.purebasic.fr/german/viewtopi ... preprocess eine macrofreie Version der Klassen erzeugen. Also erstmal alle Klassen in einer Datei, PreProcess aufrufen und dann diese in den eigenen Programmen nutzen. Die "test.pb.pre.pb" in der 7z wurde so erzeugt. Die Reste des Module "class" kann man mehr löschen, da taucht eh nur eine Konstante auf, die nirgends mehr gebraucht wird. Ist auch ganz praktisch, wenn man sehen will, wie das ganze Arbeitet.