Mal wieder etwas zum Thema stabiler Code. Ihr kennt das Problem, wenn eine Objekt nicht sauber instanziert wurde, weil z.B. der Konstruktor (Create()) mal wieder vergessen wurde. Nun wollen wir versuchen, unsere Klasse etwas abzusichern, dass sie nicht mehr einfach mit den üblichen Access-Violations crasht. Denn wenn dies passiert, kommt häufig auch der Delphi-Debuger etwas aus dem Tritt. Das Ziel soll also sein, eine saubere Assertion zu generieren, wenn das Objekt nicht sauber angelegt haben.
Folgender Code sollte also in unserer Klasse eine Assertion auslsen:
procedure TForm1.Button1Click(Sender: TObject);
const
MySecureObject : TMySecureBaseClass = nil;
begin
// hier vergessen wir den Constructor Create()
try
MySecureObject.Something;
finally
MySecureObject.Free;
end;
end;
Dies ist sicher nicht für jede kleine Nebenklasse interessant, sondern eher für die zentralen Klassen, welche vielerorts verwendet werden.
Die einen oder anderen denken sich nun wohl: "eigentlich ist das ja genau das, was der Free() bzw. Destroy() macht". GENAU! Nur macht der Free das intern mit Assembler-Code, was wir doch brav sein lassen wollen . Wie Ihr ja sicher alle wisst, könnt Ihr einen Free immer und so viel wie mglich aufrufen. Der Destructor Destroy() wird von Free() nur dann aufgerufen, wenn die Klasse auch wirklich noch existiert. Und genau sowas wollen wir in unserer Klasse auch haben.
Theorie
Überlegen wir uns doch mal, was denn eigentlich bei einem Prozedur bzw. Methoden-Aufruf geschieht? Bei einem normalen Prouedur-/Funktionsaufruf wird lediglich ein Jump an eine bestimmt Memory-Stelle ausgeführt (nach Möglichkeit dahin, wo die Prozedur liegt *g*). Hierbei unterschlage ich jetzt einfach mal so Details wie das mit Parameter, Result's und den Registern funtzt - ist in dem Moment nicht weiter wichtig. Ein Methodenaufruf ist nichts weiter als eine Prozedure-/Function-Aufruf, mit dem kleinen Unterschied, dass als Parameter noch einen Pointer auf das Objekt mitgegeben wird (self). Es spricht also nichts dagegen, eine Methode aufzurufen (anzuspringen), ohne den Constructor ausgeführt zu haben.
Dies ist auch in der Praxis so, denn eine Access-Violation kriegt man erst, wenn man effektiv auf Daten des Objektes zugreifen will, da ja der Self-Pointer nicht brauchbar ist. Ebenfalls ein Problem sind virtual und dynamic Methoden, da diese die VMT (Virtual Method Table) benötigen, welche ebenfalls im Constructor erstellt wird.
Umsetzung
Also, wir bruchten eine nicht-virtuelle Methode. In dieser knnen wir den Self-Pointer testen, und ggf. eine Assertion auslsen. Ist self ok bzw. mindestens nicht NIL, dann rufen wir z.B. die virtuelle Methode auf oder arbeiten mit Felder (Daten) des Objektes. Der Beispielcode sieht dann folgendermassen aus:
interface
type
TMySecureBaseClass = class
protected
procedure DoSomething; virtual;
public
procedure Something;
end;
implementation
procedure TForm1.Button1Click(Sender: TObject);
const
MySecureObject : TMySecureBaseClass = nil;
begin
//MySecureObject := TMySecureBaseClass.Create;
try
MySecureObject.Something;
finally
MySecureObject.Free;
end;
end;
{ TMySecureBaseClass }
procedure TMySecureBaseClass.DoSomething;
begin
ShowMessage('Hallo Welt');
end;
procedure TMySecureBaseClass.Something;
begin
assert(assigned(self), 'Objekt wurde noch nicht instanziert!');
DoSomething;
end;
Aktiviert Ihr die Create-Zeile luft das Progi mit der entsprechenden Meldung. Ohne den Create erhaltet Ihr die gewünschte Assertion.
Natürlich könnte dieses Vorgehen noch weiter ausgebaut werden, indem man den Check auslagert und zu beginn jeder Methode aufruft. Es wäre auch einmal einen Versuch wert, wenn Self NIL ist, die Klasse at the fly zu erstellen und dem Self-Pointer zuzuweisen (evtl. InstanceClass). Anstatt einem Assert könnte man auch etwas mehr Intelligenz mittels IF einbauen. Falls jemand das noch weiter ausbaut, bin ich natürlich auf Feedback gespannt.