Module_Class - Klassen mit "IncludeFile" und ohne Verwaltung

Hier könnt Ihr gute, von Euch geschriebene Codes posten. Sie müssen auf jeden Fall funktionieren und sollten möglichst effizient, elegant und beispielhaft oder einfach nur cool sein.
GPI
Beiträge: 1511
Registriert: 29.08.2004 13:18
Kontaktdaten:

Module_Class - Klassen mit "IncludeFile" und ohne Verwaltung

Beitrag von GPI »

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:

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)
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.

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 "----"
Jetzt müssen nur noch alle Member definieren:

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)
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:

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 "----"
Vererbungen sind auch möglich. Hier wird Klasse res3 mit Parent Res2 erstellt:

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()
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.

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()
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 )

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
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.

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
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

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
nach

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() ) : 
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:

Code: Alles auswählen

Macro Extends(): $nil: EndMacro  :  : 
Macro class(): res: EndMacro  :  : 
IncludeFile(class: : #class_includepath+"module_class_declaration.pbi")
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.
CodeArchiv Rebirth: Deutsches Forum Github Hilfe ist immer gern gesehen!