Einzelnen Beitrag anzeigen

Thom

Registriert seit: 19. Mai 2006
570 Beiträge
 
Delphi XE3 Professional
 
#1

Intelligente Objekte - automatische Freigabe von Referenzen

  Alt 5. Mär 2012, 03:40
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;
Listing 1
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;
Listing 2
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;
Listing 3
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;
Listing 4
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;
Listing 5
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;
Listing 6
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;
Listing 7
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;
Listing 8
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;
Listing 9
Es unterstützt drei Verfahren zur Referenzverwaltung:
  1. Die automatische Referenz auf Variablen im Speicher (lokal und global) unter Verwendung von Interfaces.
  2. Die manuelle Referenz auf Variablen im Speicher (lokal und global) unter Verwendung der Methode AddRef().
  3. 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;
Listing 10
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;
Listing 11
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;
Listing 12
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.
Thomas Nitzschke
Google Maps mit Delphi
  Mit Zitat antworten Zitat