Hiermit möchte ich ein Konzept zur automatischen Freigabe von Referenzen auf nicht mehr vorhandene Objekte zur Diskussion stellen.
Über das automatische Rücksetzen von Referenzen wurde schon viel diskutiert. Einer der Threads zu diesem Thema ist
stahli's
Referenzen auf ungültige Objekte.
Jedem ist (sollte) bekannt (sein), daß nach
Delphi-Quellcode:
var
o1, o2: TObject;
begin
[...]
o1:=TObject.Create;
o2:=o1;
o1.Free;
[...]
end;
sowohl o1 als auch o2 noch auf ein (nicht mehr gültiges) Objekt verweisen. Auch das verteufelte
FreeAndNil schafft nur teilweise Abhilfe:
Delphi-Quellcode:
var
o1, o2: TObject;
begin
[...]
o1:=TObject.Create;
o2:=o1;
FreeAndNil(o1);
[...]
end;
In diesem Fall bleibt o2 erhalten, auch wenn o1 auf
nil gesetzt wurde.
Obwohl es zu diesem Thema meistens negative Meinungen gibt (braucht man nicht, Blödsinn, zeugt von einem schlechten Design, ...), existieren Fälle, in denen das automatische Setzen von ungültigen Referenzen auf
nil von entscheidender Bedeutung ist.
Dazu ein praktisches Beispiel mit dem
Delphi Framework für Google Maps:
Delphi-Quellcode:
TForm1 = class([...])
[...]
private
FMyMarker: TMarker; //Stelle 1
[...]
end;
procedure TForm1.Button1Click(Sender: TObject);
begin
with Script do
begin
FMyMarker:=New(Google.Maps.Marker); //Stelle 2
FMyMarker.Position:=New(Google.Maps.Point(10,20));
FMyMarker.Map:=Maps[0];
end;
end;
In der Methode
Button1Click werden zwei Objekte neu erstellt:
TMarker und
TPoint. Der Marker wird an zwei Stellen gespeichert: In
FMyMarker (Stelle 1) und - nicht offensichtlich - in der Marker-Liste des Script-Objektes, also
TScript.Markers (Stelle 2). Zusätzlich melden sich beide Objekte in einer frameworkinternen Liste an, um Speicherlecks zu vermeiden. Das
TPoint-Objekt wird so spätestens bei Beendigung des Programmes freigegeben.
Werden jetzt aber massenhaft neue Marker angelegt, steigt der Speicherverbrauch duch die Punkte, obwohl sie eigentlich gar nicht mehr benötigt werden. Abhilfe würde eine "ordentlichere" Programmierung im Delphi-Stil schaffen:
Delphi-Quellcode:
procedure TForm1.Button1Click(Sender: TObject);
var
Point: TPoint;
begin
with Script do
begin
Point:=New(Google.Maps.Point(10,20));
try
FMyMarker:=New(Google.Maps.Marker);
FMyMarker.Position:=Point;
FMyMarker.Map:=Maps[0];
finally
Point.Free;
end;
end;
end;
Leider macht das den Quelltext nicht gerade übersichtlicher, kompakter oder JavaScript-ähnlich. Abhilfe würde hier die Verwendung von Interfaces mit ihrer automatischen Referenzzählung bieten:
Delphi-Quellcode:
procedure TForm1.Button1Click(Sender: TObject);
var
Point: IPoint;
begin
with Script do
begin
Point:=New(Google.Maps.Point(10,20));
FMyMarker:=New(Google.Maps.Marker);
FMyMarker.Position:=Point;
FMyMarker.Map:=Maps[0];
end;
end;
oder so kurz wie in Listing 3 ohne lokale Variable.
TPoint würde in diesem Fall nach der Zuweisung zu
TMyMarker.Position sofort automatisch freigegeben oder bei Listing 5 nach Verlassen der Methode. So weit - so gut.
Das Marker-Objekt würde so aber - wenn es nicht in FMyMarker (vom Typ
IMarker) referenziert würde - sofort nach Beendigung der Methode
Button1Click wieder freigegeben und so von der Karte verschwinden. Um da zu vermeiden, muß die Liste
TScript.Markers Interfaces verwenden. Dadurch ergibt sich aber eine äußerst ungünstige Situation: Eine Zuweisung von
nil zu
FMyMarker bewirkt augenscheinlich gar nichts und erst das zusätzliche Löschen mit
TScript.Markers.Delete(...) oder
TScript.Markers.Remove(...) gibt den Marker tatsächlich frei:
Delphi-Quellcode:
procedure TForm1.Button2Click(Sender: TObject);
begin
with Script do
begin
Markers.Remove(FMyMarker);
FMyMarker:=nil;
end;
end;
Die Notwendigkeit der mehrfachen Freigabe macht die Programmierung nicht gerade übersichtlicher und bedeutet gegenüber der Nutzung reiner Objekte einen Rückschritt.
Wäre es nicht bedeutend einfacher - zusätzlich zu den Möglichkeiten, die die Referenzzählung über ein Interface bietet -, ein Objekt explizit freigeben zu können - unabhängig davon, wieviele Referenzen noch bestehen -, als dessen Folge das Objekt aktiv alle Verweise auf
nil setzt und sich in allen Listen abmeldet?
Delphi-Quellcode:
procedure TForm1.Button2Click(Sender: TObject);
begin
FMyMarker.Free;
end;
Daraufhin wäre
FMyMarker nil und der Marker ist nicht mehr in der Liste
TScript.Markers enthalten.
Anders herum entfernt
Delphi-Quellcode:
procedure TForm1.Button2Click(Sender: TObject);
begin
Script.Markers[...].Free;
//oder
Script.Markets.Delete(...);
//oder
Script.Markers.Remove(...);
end;
den Marker aus der Liste und setzt
FMyMarker auf
nil.
Das wäre ein erheblicher Fortschritt, würde die Vorteile der Verwendung von reinen Objekten mit denen von Interfaces verbinden und die Möglichkeiten sogar noch erweitern.
Über derartige intelligente Zeiger und Objekte wurde schon einiges geschrieben - allerdings setzen alle mir bekannten Lösungen für Delphi auf die Verwendung von Generics und/oder anonymen Methoden und schließen damit ältere Compiler aus. Das Delphi Framework für Google Maps soll aber auch weiterhin ab Delphi 5 verwendbar sein.
Deshalb wurde folgendes Interface entworfen:
Delphi-Quellcode:
type
INotify = interface(IInterface)
['{D37C5177-D900-4D99-A97B-A341865B258D}']
procedure AddRef(const Ref);
procedure AddNotify(const Notify: INotify);
procedure Free;
procedure FreeNotify(const Notify: INotify);
procedure RemoveRef(const Ref);
procedure RemoveNotify(const Notify: INotify);
function _ReleaseSave: Integer;
function GetObject: TObject;
function GetRefCount: Integer;
function IsDestroying: Boolean;
property RefCount: Integer read GetRefCount;
end;
Es unterstützt drei Verfahren zur Referenzverwaltung:
- Die automatische Referenz auf Variablen im Speicher (lokal und global) unter Verwendung von Interfaces.
- Die manuelle Referenz auf Variablen im Speicher (lokal und global) unter Verwendung der Methode AddRef().
- Die Benachrichtigung über die Freigabe unter Verwendung der Methode AddNotify().
Zusätzlich kann zur Kontrolle der Stand des Interface-Referenzzählers über
RefCount beziehungsweise
GetRefCount ausgelesen werden.
Wird das Objekt, das
INotify implementiert, freigegeben, informiert es alle angemeldeten Schnittstellen über
FreeNotify und setzt alle Speicherreferenzen auf
nil.
Die Funktion
GetObject unterstützt ältere Compiler, die noch keinen Interface-to-Object-Cast besitzen.
IsDestroying liefert
true, sobald sich das Objekt hinter dem Interface in der Methode
Destroy befindet. Das ist notwendig bei Listen - genauer gesagt bei
TObjectList -, um einen Mehrfachaufruf des Destructors zu vermeiden.
Zu erwähnen wäre noch
_ReleaseSave: Damit wird das unangenehme Verhalten von
TInterfacedObject vermieden, daß das Objekt nach Abfrage des Interfaces gleich wieder zerstört wird, wenn vorher der Referenzzähler nicht erhöht wurde:
Delphi-Quellcode:
procedure TForm1.Button3Click(Sender: TObject);
var
o: TInterfaceObject;
begin
o:=TInterfacedObject.Create;
if Supports(o,IInterface) then ;
o. ...; //<- geht schief, da das Objekt inzwischen freigegeben wurde
end;
Konkret sieht das so aus:
Delphi-Quellcode:
procedure TForm1.Button4Click(Sender: TObject);
var
o1, o2: INotifyObject;
begin
o1:=TNotifyObject.Create;
o2:=o1;
o1.Free;
//sowohl o1 als auch o2 sind jetzt nil!!!
end;
Listen benutzen die Benachrichtigungsmethode, da direkte Speicherreferenzen fatale Fehler ergeben würden: Das Einfügen und Entfernen von Elementen führt zu einer Verschiebung der Zeiger und damit zu veränderlichen Speicheradressen. Bei Listen, die das
INotify-Interface unterstützen, sieht das folgendermaßen aus:
Delphi-Quellcode:
procedure TForm1.Button5Click(Sender: TObject);
var
o: TNotifyObject;
l: TList;
begin
o:=TNotifyObject.Create;
o.AddRef(o); //<- bei Freigabe Variable auf nil setzen
l:=TList.Create;
try
l.Add(o);
o.Free; //Abmeldung bei der Liste und o auf nil setzen
ShowMessage(IntToStr(l.Count));
if not assigned(o)
then ShowMessage('o=nil');
finally
l.Free
end;
end;
Eine wichtiger Unterschied besteht allerdings bei den Aufrufen von
TNotifyObject.Free und
INotify.Free:
Die Methode
Free des Objektes kann auch bei
nil ausgeführt werden, da sie immer existiert und erst bei der Ausführung getestet wird, ob das Objekt vorhanden ist. Im Gegensatz dazu führt der Versuch, die Interface-Methode bei
nil aufzurufen, zu einer Zugriffsverletzung.
Geplant ist, diese Technik in der kommenden Version der Frameworks einzusetzen. Im Gegensatz zu
stahli's Lösung ist sie aber so allgemein gehalten, daß alle Objekte, die das
INotify-Interface unterstützen oder einfach von
TNotifyObject abgeleitet werden, an diesem Mechanismus teilhaben können. Die wichtigsten Listen
TList,
TObjectList,
TThreadList und
TInterfaceList wurden mit dieser Schnittstelle ausgerüsten und stehen so allgemein zur Verfügung.