Delphi-PRAXiS

Delphi-PRAXiS (https://www.delphipraxis.net/forum.php)
-   Object-Pascal / Delphi-Language (https://www.delphipraxis.net/32-object-pascal-delphi-language/)
-   -   Delphi Interessantes Destruktor Problem (https://www.delphipraxis.net/157306-interessantes-destruktor-problem.html)

sx2008 7. Jan 2011 07:15

Delphi-Version: 5

Interessantes Destruktor Problem
 
Dieser Thread ist ein Spin-Off von Delphi Kurzreferenz

Dort hat Deep Sea folgenden Code gepostet:
Delphi-Quellcode:
constructor TAbgeleiteteKlasse.Create;
begin
  FStream := TMemoryStream.Create;
  inherited Create(FStream);
end;

destructor TAbgeleiteteKlasse.Destroy;
begin
  inherited; // Könnte ja auf den übergebenen Stream noch zugreifen.
  FStream.Free;
end;
Auf den ersten Bick sieht der Code korrekt aus, aber er trägt eine Zeitbombe in sich.
Nach dem Aufruf von inherited im Destruktor in der gesamte Speicher des Objekts freigegeben.
Es ist daher verboten nach diesem Zeitpunkt auf FStream zuzugreifen.
Das MemoryStream-Objekt selbst ist zwar noch intakt, aber die Variable FStream ist nicht mehr gültig.

In den allermeisten Fällen geht ein Zugriff auf diesen freigegebenen Speicher glimpflich ab.
Wenn man allerdings einen Memory-Manager (FasttMM4) benützt und dieser den freigegebenen Speicher
mit bestimmten Gardbytes füllt, dann wird das Problem offensichtlich.
Oder wenn ein anderer Thread zufällig gerade den Speicher bekommt der soeben im Destruktor freigegeben wurde dann gibt das ganz bösartige Probleme.

Ich behaupte also man darf so wie oben nicht programmieren und lade jeden ein sich zu überlegen, wie man das Problem umgehen könnte.

mleyen 7. Jan 2011 07:39

AW: Interessantes Destruktor Problem
 
Ich glaub ich habe das Problem nicht ganz verstanden.
Imho ist es nichtmal OOP-konform, wenn ein Objekt nach seiner Selbstzerstörung noch etwas macht.

Neutral General 7. Jan 2011 08:12

AW: Interessantes Destruktor Problem
 
Naja es geht wahrscheinlich nur darum, dass im Destructor das inherited ganz am Schluss aufgerufen werden soll/muss und nicht wie sonst meistens am Anfang der Methode. Aber die Codevervollständigung von neueren Delphiversionen legt Destructoren direkt schon so an:

Delphi-Quellcode:
destructor TKlasse.Destroy;
begin

  inherited;
end;
Ist also schon in der Codevervollständigung ein kleiner Wink mit dem Zaunpfahl ;)

Bummi 7. Jan 2011 08:13

AW: Interessantes Destruktor Problem
 
@sx2008

wie kommst Du darauf daß der Code korrekt aussähe..:?::?:

generic 7. Jan 2011 08:50

AW: Interessantes Destruktor Problem
 
Vererbung ist für diese Problem wohl nicht die Lösung.
Ein Composit(-Pattern) wäre besser geeignet.

Uwe Raabe 7. Jan 2011 08:53

AW: Interessantes Destruktor Problem
 
Zitat:

Zitat von sx2008 (Beitrag 1072844)
Ich behaupte also man darf so wie oben nicht programmieren

Ich behaupte das Gegenteil!

Erzeuge mal eine Instanz der folgenden Klasse und gib sie wieder frei:
Delphi-Quellcode:
type
  TMyObject = class
  public
    destructor Destroy; override;
    procedure FreeInstance; override;
  end;

destructor TMyObject.Destroy;
begin
  ShowMessage('Destroy before');
  inherited;
  ShowMessage('Destroy after');
end;

procedure TMyObject.FreeInstance;
begin
  ShowMessage('FreeInstance');
  inherited;
end;
Ich bekomme folgende Meldungen:

Destroy before
Destroy after
FreeInstance

Da die Freigabe des Speichers erst in TObject.FreeInstance erfolgt, ist die Instanz während des gesamten Destroy noch existent. (Getestet in XE ohne FastMM4)

Neutral General 7. Jan 2011 08:56

AW: Interessantes Destruktor Problem
 
Zitat:

Zitat von Uwe Raabe (Beitrag 1072862)
Zitat:

Zitat von sx2008 (Beitrag 1072844)
Ich behaupte also man darf so wie oben nicht programmieren

Ich behaupte das Gegenteil!

Naja trotzdem sollte man so nicht programmieren. Nach dem inherited wurden alle Objekte der Basisklasse(n) schon freigegeben (davon ausgegangen, dass die Basisklassen sauber programmiert wurden). Da kann man böse auf die Nase fallen.

Deep-Sea 7. Jan 2011 09:01

AW: Interessantes Destruktor Problem
 
Delphi-Quellcode:
constructor TGrundklasse.Create(AStream: TStream);
begin
  inherited Create;
end;

destructor TGrundklasse.Destroy;
begin
  inherited;
end;

constructor TAbgeleiteteKlasse.Create;
begin
  FStream := TMemoryStream.Create;
  inherited Create(FStream);
end;

destructor TAbgeleiteteKlasse.Destroy;
begin
  inherited; // Könnte ja auf den übergebenen Stream noch zugreifen.
  FStream.Free;
end;
Destroy von TGrundklasse:
Code:
Unit1.pas.47: inherited;
0046491F 8BD3             mov edx,ebx
00464921 80E2FC          and dl,$fc
00464924 8BC6             mov eax,esi
00464926 E8B9F1F9FF      call TObject.Destroy
Unit1.pas.48: end;
0046492B 84DB            test bl,bl
0046492D 7E07             jle $00464936 // Springt um ClassDestroy herum.
0046492F 8BC6             mov eax,esi
00464931 E846F6F9FF      call @ClassDestroy // Ruft FreeInstance auf.
00464936 5E              pop esi
00464937 5B              pop ebx
00464938 C3               ret
00464939 8D4000           lea eax,[eax+$00]
Richtig, ClassDestroy, was FreeInstance aufrufen wird, steht hier drin. ABER: Durch den bedingten Sprungbefehl jle wird es hier nicht aufgerufen!

Destroy von TAbgeleiteteKlasse:
Code:
Unit1.pas.58: inherited;
00464993 8BD3             mov edx,ebx
00464995 80E2FC          and dl,$fc
00464998 8BC6             mov eax,esi
0046499A E875FFFFFF      call TGrundklasse.Destroy
Unit1.pas.59: FStream.Free;
0046499F 8B4604           mov eax,[esi+$04]
004649A2 E84DF1F9FF      call TObject.Free
Unit1.pas.60: end;
004649A7 84DB            test bl,bl
004649A9 7E07             jle $004649b2 // Springt nicht.
004649AB 8BC6             mov eax,esi
004649AD E8CAF5F9FF      call @ClassDestroy // Ruft FreeInstance auf.
004649B2 5E              pop esi
004649B3 5B              pop ebx
004649B4 C3               ret
004649B5 8D4000           lea eax,[eax+$00]

@Neutral General:
Zwingt dich auch keiner zu, wenn du schnell den Überblick verlierst. Aber falsch ist es immer noch nicht.


Danke Uwe, endlich mal jemand der auf meiner Seite steht :-D

himitsu 7. Jan 2011 09:04

AW: Interessantes Destruktor Problem
 
Zitat:

Delphi-Quellcode:
inherited; // Könnte ja auf den übergebenen Stream noch zugreifen.
FStream.Free;

Wenn FStream in der TAbgeleiteteKlasse deklariert ist, wie/wieso sollten dann dessen Vorfahre darauf zugreifen? Der Vorfahre kennt FStream doch nicht.
Es ist eher andersrum, also daß TAbgeleiteteKlasse auf Eigenschaften des Vorfahren zugreift.
Und wenn FStream im Vorfahren deklariert ist, dann ist dieser für dessen Erzeugung und Freigabe verantwortlich, womit dieses nicht in den Nachfahren reingehören würde.

Also im Constructur und anderen erzeugenden Routinen Inherited grundsätzlich als Erstes und im Destructor, sowie anderen freigebenden Routinen als Letzes.
Und sonst je nach dem, wie Dinge aus dem Vorfahren gebraucht/erzeugt/freigegeben werden.

Neutral General 7. Jan 2011 09:07

AW: Interessantes Destruktor Problem
 
Zitat:

Zitat von DeepSea
@Neutral General:
Zwingt dich auch keiner zu, wenn du schnell den Überblick verlierst. Aber falsch ist es immer noch nicht.

Ne "falsch" ist es nicht. Aber man muss aufpassen was man macht und das kann eben schnell in die Hose gehen. Wenn man das inherited als letztes aufruft, dann ist man auf der sicheren Seite und kann im Destructor vor dem inherited machen was man will ohne befürchten zu müssen, dass einem alles um die Ohren fliegt.

Abgesehen davon ist es deutlich sauberer das inherited in Destructoren immer (!) als letztes aufzurufen. So wird das Objekt ganz geordnet "von oben nach unten" abgeräumt. Wenn jeder sein inherited irgendwo an den Anfang oder in die Mitte des Destructors setzt ist es als würde man aus nem Turm irgendwo in der Mitte ein Stein rausziehen... (:arrow: Jenga)

mkinzler 7. Jan 2011 09:08

AW: Interessantes Destruktor Problem
 
Auch wenn es in diesem Beispiel geht, da ja Stream ein übergebener Parameter ist, sollte man die Reihenfolge konsequent einhalten.
Denn somst könnte es bei späteren Änderungen im Code zu Missverständnissen führen.
Ich würde auch zudem keine "externen" Objekte in einem Konstruktor erzeugen.

Uwe Raabe 7. Jan 2011 09:09

AW: Interessantes Destruktor Problem
 
Zitat:

Zitat von Neutral General (Beitrag 1072865)
Naja trotzdem sollte man so nicht programmieren. Nach dem inherited wurden alle Objekte der Basisklasse(n) schon freigegeben (davon ausgegangen, dass die Basisklassen sauber programmiert wurden). Da kann man böse auf die Nase fallen.

Natürlich muss man wissen, was man tut - das gilt immer und überall beim Programmieren. Es kann ja durchaus sein, daß im inherited eine virtuelle Methode aufgerufen wird, die in einer abgeleiteten Version auf einige der Instanz-Felder zugreift. Dann müssen diese Felder während des inherited Destroy noch gültig sein. TList ruft z.B. im Destroy ein Clear auf, das in einer abgeleiteten Klasse überschrieben sein kann und auf irgendwelche Felder in dieser abgeleiteten Klasse zugreift. (Interessanterweise ruft TList.Destroy gar kein inherited auf!)

Die Aussage "man sollte so nicht programmieren" kann ich also in keiner Weise nachvollziehen.

Nur ein paar Beispiele aus Classes.pas:

Delphi-Quellcode:
destructor TRegGroup.Destroy;
destructor TThreadList.Destroy;
destructor TStringList.Destroy;
destructor TThread.Destroy;
destructor TBasicAction.Destroy;
destructor TDataModule.Destroy;

Bummi 7. Jan 2011 09:14

AW: Interessantes Destruktor Problem
 
Delphi-Quellcode:
unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls;

type

  TMyObject = class
  Private
    FMyTest:TStringList;
    function GetInfo: String;
  public
    destructor Destroy; override;
    Constructor Create;virtual;
    procedure FreeInstance; override;
    Property Info:String Read GetInfo;
  end;
  TMyObject2 = class(TMyObject)
  public
    destructor Destroy; override;
    Constructor Create;override;
    procedure FreeInstance; override;
 end;
  TForm1 = class(TForm)
    ADODataSet1: TADODataSet;
    Button1: TButton;
    procedure Button1Click(Sender: TObject);
  private
    { Private-Deklarationen }
  public
    { Public-Deklarationen }
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

procedure TForm1.Button1Click(Sender: TObject);
begin
  With TMyObject2.Create do Free;

end;
{ TMyObject }

constructor TMyObject.Create;
begin
  inherited;
  FMyTest:=TStringList.Create;
  FMyTest.Add('Text');
end;

destructor TMyObject.Destroy;
begin
  FMyTest.Free;
  inherited;
end;

procedure TMyObject.FreeInstance;
begin
  inherited;

end;

function TMyObject.GetInfo: String;
begin
   Result := FMyTest.Text;
end;

{ TMyObject2 }

constructor TMyObject2.Create;
begin
  inherited;

end;

destructor TMyObject2.Destroy;
begin
  Showmessage('Vor inherited:' +Info);
  inherited;
  Showmessage('Nach inherited:' +Info);
end;

procedure TMyObject2.FreeInstance;
begin
  inherited;

end;

end.

mleyen 7. Jan 2011 09:16

AW: Interessantes Destruktor Problem
 
Zitat:

Zitat von Uwe Raabe (Beitrag 1072871)
Natürlich muss man wissen, was man tut
...
Die Aussage "man sollte so nicht programmieren" kann ich also in keiner Weise nachvollziehen.

Da haben wir den Knackpunkt. Wenn man zB im Team arbeitet, kann es sein das andere es nicht wissen was derjenige da getan hat, übernimmt aber das inherited immer am Anfang.

Die Frage ist nicht warum man so etwas nicht tun sollte.
Die Frage ist: Gibt es irgend einen Grund warum man sowas machen sollte?

Deep-Sea 7. Jan 2011 09:18

AW: Interessantes Destruktor Problem
 
@Bummi:
Und was soll uns der Code zeigen?

@mleyen:
Darum sollte man auch LERNEN wie etwas funktioniert, anstatt einfach irgendeine Aussage von wegen "Es muss so sein, frag nicht warum, es ist halt so!" zu glauben :roll:
Und wenn man schon mit so einem Anfänger im Team arbeitet, gibt es ja noch die guten alten Kommentare, um zu erklären, warum hier etwas nach inherited steht obwohl das ja sooo böse ist.
Und einen Grund gibt es auch, dass zu tun: Wenn man z.B. eine von Uwe Raabe genannte Klasse ableiten will, kann es u.U. unerlässlich sein.

s.h.a.r.k 7. Jan 2011 11:30

AW: Interessantes Destruktor Problem
 
Zitat:

Zitat von himitsu (Beitrag 1072868)
Zitat:

Delphi-Quellcode:
inherited; // Könnte ja auf den übergebenen Stream noch zugreifen.
FStream.Free;

Wenn FStream in der TAbgeleiteteKlasse deklariert ist, wie/wieso sollten dann dessen Vorfahre darauf zugreifen? Der Vorfahre kennt FStream doch nicht.
Es ist eher andersrum, also daß TAbgeleiteteKlasse auf Eigenschaften des Vorfahren zugreift.
Und wenn FStream im Vorfahren deklariert ist, dann ist dieser für dessen Erzeugung und Freigabe verantwortlich, womit dieses nicht in den Nachfahren reingehören würde.

Ich weiß echt nicht, warum es hier so viele Diskussionen um sowas geben kann. Genau das war es, an was ich gedacht hatte, als ich den Code und vor allem den Kommentar dazu gelesen hatte. Das widerspricht ganz dezent dem, was ich mir unter OOP vorstelle.

Uwe Raabe 7. Jan 2011 11:38

AW: Interessantes Destruktor Problem
 
Zitat:

Zitat von himitsu (Beitrag 1072868)
Zitat:

Delphi-Quellcode:
inherited; // Könnte ja auf den übergebenen Stream noch zugreifen.
FStream.Free;

Wenn FStream in der TAbgeleiteteKlasse deklariert ist, wie/wieso sollten dann dessen Vorfahre darauf zugreifen? Der Vorfahre kennt FStream doch nicht.

Wie ich an anderer Stelle schon ausgeführt habe: Es kann sein, daß im inherited des Vorfahren eine virtuelle Methode aufgerufen wird, die im Kontext des Nachfahren halt doch auf FStream zugreift. Das Beispiel von TList.Destroy, daß virtuell Clear aufruft kommt in meinen Programmen bestimmt einige Male vor. Würde ich dort das Äquivalent von FStream vor dem inherited schon freigeben, würde das bestenfalls nur Zugriffsverletzungen zur Folge haben.

SirThornberry 7. Jan 2011 11:57

AW: Interessantes Destruktor Problem
 
Also ganz in Ordnung finde ich den Code auch nicht allerdings wegen etwas anderem.
Delphi-Quellcode:
constructor TAbgeleiteteKlasse.Create;
begin
  FStream := TMemoryStream.Create;
  inherited Create(FStream);
end;

destructor TAbgeleiteteKlasse.Destroy;
begin
  [...]
  FStream.Free;
end;
Und zwar ist hier ersichtlich das der Constructor der Vorfahrenklasse einen Constructor hat dem ein Stream übergeben wird.
Auch wenn der Constructor der neuen Klasse diesen Parameter nicht mehr hat ist es trotzdem noch möglich den Constructor der Vorfahrenklasse aufzurufen und den Stream zu übergeben. In dem Fall ist es Fatal das im neuen Destructor einfach mein übergebener Stream frei gegeben wird.

Bsp.:
Delphi-Quellcode:
var
  mystream: TStream;
  tmp: TAbgeleiteteKlasse;
begin
  MyStream := TMemoryStream.Create();
  tmp := TAbgeleiteteKlasse.Create(myStream);
  [...]
  tmp.Free;
  //jetzt ist plötlizlich mystream freigegeben obwohl das bei der Vorfahrenklasse noch nicht der Fall war
  [...]
Also ein eindeutiger Designfehler. Das worüber hier die ganze Zeit diskutiert wird kann ich nicht nachvollziehen denn es ist tatsächlich ab und zu der Fall das man erst nach dem inherited im Destructor etwas freigeben kann. Das ist zum Beispiel der Fall wenn im Destructor der Klasse von der man ableitet noch OnChange-Ereignisse/Speichern-Methoden etc. aufgerufen werden die durch Vererbung Dinge aus dem Nachfahren verwenden.

Deep-Sea 7. Jan 2011 12:01

AW: Interessantes Destruktor Problem
 
@SirThornberry:
Es war ja auch nur ein Beispiel, was mir im damaligen Thread spontan eingefallen ist. Über Sinn oder Unsinn einen Konstruktor derart zu überschreiben ging es bei der Diskussion aber auch nie :wink:

PS: Der "alte" Konstruktor, der den Stream erwartet, könnte z.B. (strict) protected sein, so kann man ihn nicht so aufrufen, wie in deinem Beispiel ...

SirThornberry 7. Jan 2011 12:14

AW: Interessantes Destruktor Problem
 
Achso. Auf jeden Fall ist an dem diskutierten nichts verwerfliches. Wie bereits beschrieben wird der Speicher schließlich vorm Constructor-Aufruf reserviert und nach dem Destructor-Aufruf freigegeben. Und auch in anderen Programmiersprachen ist das nicht viel anders.


Alle Zeitangaben in WEZ +1. Es ist jetzt 01:14 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