Delphi-PRAXiS

Delphi-PRAXiS (https://www.delphipraxis.net/forum.php)
-   Tutorials und Kurse (https://www.delphipraxis.net/36-tutorials-und-kurse/)
-   -   [Tutorial] Den Destruktor verstehen (https://www.delphipraxis.net/167726-%5Btutorial%5D-den-destruktor-verstehen.html)

sx2008 15. Apr 2012 06:20

[Tutorial] Den Destruktor verstehen
 
Wenn in Delphi ein Objekt aus dem Leben scheidet dann gibt es nur einen einzigen Weg dies zu tun -
über den Destruktor.

Borland hat den Destruktor schon in der Vaterklasse TObject deklariert
Delphi-Quellcode:
TObject = class
...
public
...
  destructor Destroy; virtual;
end;
Zu jeder Klasse gibt eine VMT (virtual Method Table) und die Destroy-Methode hat dort einen festen Eintrag.
Der Index innerhalb der VMT ist mit der Konstanten vmtDestroy=-4 festgelegt.

Es gibt nur einen einzigen Destruktor.
Daraus kann man die Nebenbedingungen schlussfolgern
* der Destructor muss immer Destroy heisen
* der Destruktor kann keine Parameter haben und er hat auch keinen Rückgabewert
* der Destruktor muss immer mit override überschrieben werden
* der Destruktor darf keine eingeschränkte Sichtbarkeit haben; er ist also immer public

Delphi-Quellcode:
destructor Destroy; override;
Jede Abweichung von dieser Deklaration ist falsch.

Hier ist ein Beispiel mit mehreren falsch deklarierten Destruktoren:
Delphi-Quellcode:
TMeineKlasse = class(TPersistent)
private
  destructor Destroy; // Falsch - override fehlt, darf nicht private sein
public
  destructor Destroy; override; // Richtig

  destructor GibFrei; // Falscher Name

  destructor Destroy; reindroduce; // Falsch da so die VMT ignoriert wird
end;
Warum hat der Destruktor überhaupt einen Namen, wenn es sowieso nur einen einzigen gibt?
Hätte der Destruktor keinen Namen, gäbe es keine Möglichkeit, ihn direkt aufrufen

Warum ist der Destruktor mit der Sichtbarkeit public deklariert, hätte man ihn nicht auch protected machen können?
Borland wollte offensichtlich dem Programmierer die Möglichkeit geben, den Destruktor direkt aufzurufen.

Warum gibt es eigentlich nur einen einzigen Destruktor?
Wenn ein Objekt freigeben wird, dann wird es nicht mehr gebraucht.
Derjenige, der das Objekt freigibt hat oftmals kein Wissen um was für ein Objekt es sich handelt.
Genauer gesagt, der Code, der ein Objekt freigibt soll gar keine näheren Infos zu Internas des Objekt haben. (Geheimnisprinzip des OOP)
Der Besitzer des Objekts sagt "Stirb!" und das Objekt hat zu gehorchen.

Was ist der Unterschied zwischen Free und Destroy?
Free ruft Destroy auf, abr nur wenn das Objekt überhaupt existiert; also der self-Pointer nicht nil ist.
Am Besten sieht man das, wenn man sich die Free-Prozedur anschaut.
Delphi-Quellcode:
procedure TObject.Free;
begin
  if Assigned(self) then Destroy;
end;
Darf man Destroy überhaupt aufrufen? Es heisst doch immer man solle Free verwenden.
Ja, man darf Destroy direkt aufrufen und man kann so einige CPU-Takte sparen.
Aber man darf dies nur tun, wenn man 1000% sicher ist dass man das Objekt selbst erzeugt hat:
Delphi-Quellcode:
var
  meinobj : TMeineKlasse;
begin
  meinobj := TMeineKlasse.Create;
  try
    meinobj.Machwas;
    meinobj.MasWasAnderes(42);
  finally
    // ja, das ist erlaubt aber nicht empfehlenswert
    // man spart hier die Überprüfung ob meinobj <> nil ist
    // aber es besteht die Gefahr, dass der Code mal verändert wird
    // und dann die Änderung von Destroy nach Free vergessen wird
    meinobj.Destroy;
  end;
end;
Welcher Code gehört in einen Destruktor?
Auf jeden Fall sollte der Aufruf von Inherited die letzte Zeile im Destruktor sein.
Delphi-Quellcode:
destructor TMeinKlasse.Destroy;
begin
  FList.Free;
  inherited;
end;
Man sollte im Destruktor nur belegte Resourcen (also eingebette Objekte, Window-Handles) freigeben.
Grössere Aktionen wie z.B. Speichern in einer Datenbank sollten vermieden werden.

Soll ich einen Destruktor auch dann verwenden, wenn gar nicht zu tun ist?
Nein, ein Destruktor sollte man nur dann deklarieren, wenn auch etwas zu tun ist.
Delphi-Quellcode:
// schlechter Stil: unnötiger Code und unnötiger Eintrag in der VMT
destructor TMeineKlasse.Destroy;
begin
  inherited;
end;
Wenn ich direkt von TObject ableite, dann brauche ich doch das inherited im Destruktor gar nicht aufrufen, oder?
Das ist im Prinzip richtig, aber man sollte als Programmierer nicht versuchen übertrieben clever zu sein.
Niemand weiss nicht ob nicht jemand in Zukunft die Klasse von einer anderen Basisklasse ableitet
und dann entsteht ein Resourcen-/Speicherleck.

Was sind die häufigsten Fehler beim Destruktor?
Das override bei der Deklaration zu vergessen oder das inherited innerhalb des Destruktor zu vergessen sind die Klassiker.

Warum braucht man überhaupt das inherited im Destruktor? Könnte Delphi das nicht automatisch aufrufen?
Gute Frage! In anderen Programmiersprachen (z.B. C++, C#) ist es tatsächlich so,
dass automatisch die ererbten Destruktoren aufgerufen werden, ohne dass sich der Programmierer darum kümmern müsste.

himitsu 15. Apr 2012 07:13

AW: [Tutorial] Den Destruktor verstehen
 
Zitat:

Delphi-Quellcode:
// schlechter Stil: unnötiger Code und unnötiger Eintrag in der VMT

Fast richtig.
- ja, es ist unnötiger Code
- nein, es ist kein unnötiger Eintrag in der VMT

Denn die VMT gibt es so oder so.
- hat man was überschrieben, dann steht die eigene Methode drin
- wurde es nicht überschrieben, dann steht darin die letzte Methode des Vorfahren

Und das Destroy immer public sein muß, stimmt auch nicht.
Da diese Methode über die VMT und nicht über die RTTI gesucht wird, kann sie als alles deklariert überschrieben sein, da Free bei Freigabe über TObject und dessen Sichtbarkeiten geht.
Nur daß Nachfahren es schwer hätten das zu überschreiben, wenn es private ist.
Ich persönlich hätte dieses Destroy als Protected deklariert, was zu weniger Problemen geführt hätte. (kann muß Free aufrufen, da Destroy nicht von außen erreichbar ist)

Zitat:

Borland wollte offensichtlich dem Programmierer die Möglichkeit geben, den Destruktor direkt aufzurufen.
Ich hab eher das Gefühl man hat garnicht nachgedacht.
In der Deklaration von TObject findet man garnichts von Sichtbarkeiten ... das hatte man wohl einfach vergessen.
Das ist zumindestens in älteren Delphis so, was man inzwischen (D2010) geändert hat, aber vermutlich aus Gründen der Abwärtskompatibilität hat man lediglich das Public eingebaut und nicht gleich mal Einiges nach Protected verschoben.
Wobei ich diese angebliche Abwärtskomatibilität, wie gesagt, eh sinnlos finde, da es im Notfall dennoch sichtbar gemacht werden könnte, dort wo es wirklich unbedingt nötig währe.

Oftmals sind auch Methoden als Published deklariert, obwohl sie eigentlich nur als Public gedacht waren,
denn meistens wird zu Anfang garkeine Sichtbarkeit angegeben, da kommt ohne Angabe gleich die erste Methode in der Deklaration.
Und bei TPersistent-Nachfahren wurde die Standardsichtbarkeit von Public auf Published geändert wurde, womit diese Methoden dann automatisch Published sind.
Oder Schlimmer, vieles ist Private, was besser Protected gewesen währe. :cry:

Zitat:

Warum braucht man überhaupt das inherited im Destruktor? Könnte Delphi das nicht automatisch aufrufen?
Gute Frage! In anderen Programmiersprachen (z.B. C++, C#) ist es tatsächlich so,
dass automatisch die ererbten Destruktoren aufgerufen werden, ohne dass sich der Programmierer darum kümmern müsste.
Und was währe, wenn ich was überschreiben will, ohne daß die Vorfahrmethoden ausgeführt werden?
Und außerdem kann man über das Inherited entscheiden wann der Vorfahr ausgeführt wird. (zu Anfang, zwischendrin, am Ende oder garnicht)




Jetzt fehlt nur noch der Constructor.

Auch der Class Constructor und Class Destructor währen eine Erwähnung wert.
Das sind die neuen Initialization und Finalization, aber ohne gewisse Nachteile, welche viele kennen und weswegen oftmals bei NonVCL Units wie SysUtils weggelassen werden. (Dateigröße)

sx2008 15. Apr 2012 10:22

AW: [Tutorial] Den Destruktor verstehen
 
Zitat:

Zitat von himitsu (Beitrag 1161934)
Zitat:

Warum braucht man überhaupt das inherited im Destruktor? Könnte Delphi das nicht automatisch aufrufen? ...
Und was währe, wenn ich was überschreiben will, ohne daß die Vorfahrmethoden ausgeführt werden?

Beim Destruktor ist das verboten! Die Kette der Destruktoren muss abgearbeitet werden; sie darf nicht unterbrochen werden. Falls doch, droht ein Resourcen-/Speicherleck.
Eine abgeleitete Klasse kann und darf nicht wissen, was in den Basisklassen über ihr freigegeben wird.

Zitat:

Zitat von himitsu (Beitrag 1161934)
Und außerdem kann man über das Inherited entscheiden wann der Vorfahr ausgeführt wird. (zu Anfang, zwischendrin, am Ende oder garnicht)

Resourcen (Speicher, Handles,...) sollten immer in umgekehrter Weise freigegeben werden, in der sie angefordert wurden; das ist der sicherste Weg.
Die Freiheit in Delphi zuerst die ererbten Resourcen freigeben zu können und dann erst die eigenen Resourcen freizugeben sollte die absolute Ausnahme bleiben.
Ein Destruktor, der kein inherited aufruft, muss als potentielles Resourcen-/Speicherleck betrachtet werden.
Der Compiler sollte dies als Fehler betrachten oder zumindest eine Warnung ausgeben.

Zitat:

Zitat von himitsu (Beitrag 1161934)
Jetzt fehlt nur noch der Constructor. ...Class Constructor ...Class Destructor

Ich habe das Thema mit Absicht auf den Destruktor beschränkt; das erzeugt schon genügend Diskussionsstoff ;-)

himitsu 15. Apr 2012 12:04

AW: [Tutorial] Den Destruktor verstehen
 
Zitat:

Zitat von sx2008 (Beitrag 1161942)
Beim Destruktor ist das verboten! Die Kette der Destruktoren muss abgearbeitet werden; sie darf nicht unterbrochen werden. Falls doch, droht ein Resourcen-/Speicherleck.

Doch, ist es ... wenn ich den Constructor ebenfalls übergangen hab und/oder für die Freigaben selbst gesorgt hab.
(praktisch für einen Bugfix)

Zitat:

Resourcen (Speicher, Handles,...) sollten immer in umgekehrter Weise freigegeben werden, in der sie angefordert wurden; das ist der sicherste Weg.
Wenn das schon anders erstellt wurde, dann stimmt es auch wieder. :stupid:

sx2008 17. Apr 2012 06:55

AW: [Tutorial] Den Destruktor verstehen
 
Zitat:

Zitat von himitsu (Beitrag 1161953)
Zitat:

Zitat von sx2008 (Beitrag 1161942)
Die Kette der Destruktoren muss abgearbeitet werden; sie darf nicht unterbrochen werden. Falls doch, droht ein Resourcen-/Speicherleck.

Doch, ist es ... wenn ich den Constructor ebenfalls übergangen hab und/oder für die Freigaben selbst gesorgt hab.

Nein, ein Destruktor einer Klasse hat eine ganz bestimmte Verantwortung:
1.) er muss die lokalen belegten Resourcen seiner Klasse freigeben!

2.) er muss die Resourcen freigeben, die seiner Klasse überantwortet wurden
Zum Beispiel übernimmt die Klasse TComponent die Verantwortung dafür, dass alle Objekte im Components[]-Array im Destruktor entsorgt werden.

3.) er muss seiner Vaterklasse die Möglichkeit geben sich selbst aufzuräumen in dem er
Delphi-Quellcode:
inherited
aufruft.
Was im Destruktor der Vaterklasse passiert und ob dessen Konstruktor aufgerufen wurde oder nicht hat nicht zu interessieren.
Der Sourcecode der Vaterklasse muss als Blackbox betrachtet werden;
es muss völlig egal sein was in der Vaterklasse an Resourcen belegt wurde oder nicht.
Wichtig ist allein nur der Vaterklasse über den Aufruf von inherited die Chance zu geben, alle belegten Resourcen zu entsorgen.

4.) er muss so programmiert sein, dass selbst wenn ein Objekt nur teilweise initialisiert wurde (z.B. Exception im Konstructor) er gefahrlos die bislang belegten Resourcen freigibt.

Das sind die Regeln; das ist der (ungeschriebene) Vertrag der bei Ableitung einer Klasse geschlossen wurde.

Blup 19. Apr 2012 13:53

AW: [Tutorial] Den Destruktor verstehen
 
In 99,9% aller Fälle sind deine Regeln zutreffend, es kann Ausnahmefällen geben, allerdings sollte eine Regel immer eingehalten werden: Der Destructor "Destroy" muss immer in der Lage sein das Objekt korrekt freizugeben.

Ein kleines Beispiel mit Zirkelbezug:
Delphi-Quellcode:
TPartnerObject = class(TObject)
protected
  FPartner: TPartnerObject;
public
  // darf nicht mehr überschrieben werden
  destructor Destroy; override; finally;
  // kann in abgeleiteten Klassen überschrieben werden
  destructor MyDestroy(Sender: TObject); virtual;
  property Partner: TPartnerObject read FPartner write FPartner;
end;

// bleibt weiterhin voll funktionsfähig
destructor TPartnerObject.Destroy;
begin
  MyDestroy(Self);
end;

// Destroy ruft immer das für die Klasse gültige MyDestroy auf
destructor TPartnerObject.MyDestroy(Sender: TObject);
begin
  if Assigned(FPartner) and (FPartner <> Sender) then
    FPartner.MyDestroy(Sender);

  inherited Destroy;
end;


Kette := TPartnerObject.Create;
Kette.Partner := TPartnerObject.Create;
Kette.Partner.Partner := TPartnerObject.Create;
Kette.Partner.Partner.Partner := Kette;

FreeAndNil(Kette);

BUG 20. Apr 2012 01:00

AW: [Tutorial] Den Destruktor verstehen
 
Delphi-Quellcode:
Kreis := TPartnerObject.Create;
Kreis.Partner := TPartnerObject.Create;
Kreis.Partner.Partner := Kreis;
Stiel := TPartnerObject.Create;
Stiel.Partner = Kreis;
Stiel.free(); // Stackoverflow? (Ungetestet: hab grad kein Delphi zur Hand)
Wuhaha, Kreis am Stiel :stupid:

Eine Variante, die auch dieses Problem lösen sollte:
Delphi-Quellcode:
TPartnerObject = class(TObject)
private
  FDieing: boolean;
protected
  FPartner: TPartnerObject;
public
  constructor Create;
  destructor Destroy; override;
  property Partner: TPartnerObject read FPartner write FPartner;
end;

constructor TPartnerObject.create;
begin
  FDieing := false;
  FPartner := nil;
end;

destructor TPartnerObject.Destroy;
begin
  FDieing := true;
  if assigned(Partner) then
    if not FPartner.Dieing then
      FPartner.Destroy; // ist schon auf nil überprüft
  inherited;
end;
Das würde bei auch mehreren Partnern funktionieren und entspricht der Tiefensuche in einem Graphen.

Blup 20. Apr 2012 08:28

AW: [Tutorial] Den Destruktor verstehen
 
Natürlich gibt es für ein Problem immer mehrere Lösungen und in diesem konkreten Fall würde ich die Variante mit dem Flag vorziehen. Aber das ist nur ein Beispiel, das den richtigen Einsatz eines zweiten Destruktors demonstrieren soll.


Alle Zeitangaben in WEZ +1. Es ist jetzt 16:39 Uhr.

Powered by vBulletin® Copyright ©2000 - 2024, Jelsoft Enterprises Ltd.
LinkBacks Enabled by vBSEO © 2011, Crawlability, Inc.
Delphi-PRAXiS (c) 2002 - 2023 by Daniel R. Wolf, 2024 by Thomas Breitkreuz