Entwickler-Ecke

Delphi Language (Object-Pascal) / CLX - Objekt wird nicht zerstört


IHops - So 26.10.08 00:16
Titel: Objekt wird nicht zerstört
Ich habe für ein kleines "Schulprogramm" eine Klasse THaus erstellt. Objekte Haus werden dann gezeichnet, können verschoben werden ... durch Klick auf einen Button wird das Objekt auch wieder zerstört - sollte es zumindest :?


Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
destructor THaus.Destroy;
begin
  // Bild löschen
  with FContainer do
  begin
    Brush.Color := clWhite;
    Pen.Color := clWhite;
    Rectangle(FX-7,FY,FX+FWidth+7,FY-FHeight-2*FR-2);
  end;
  // Objekt entfernen
  inherited Destroy;
end;


Nunja, das Bild entfernen funktioniert, nur das Objekt an sich ist danach immer noch da. Sobald ich mit Hilfe der Scrollbars das Haus (welches eigentlich nicht mehr da ist) verschiebe, wird es wieder gezeichnet ... :shock:

Hat jemand eine Idee, wo mein Denkfehler sitzt?


Xentar - So 26.10.08 00:32

Hm.. zeig mal, die THaus aufgebaut ist, also das Interface davon.

Edit:
BTW: Wenn du das Objekt zerstören willst, brauchst du kein Rechteck mehr zu malen, da es danach ja sowieso weg ist und wieder vom Formular übermalt wird.


IHops - So 26.10.08 13:56


Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:
39:
40:
41:
42:
43:
44:
UNIT mTHaus;

interface

uses
  Graphics, Types;

type
  THaus = class

  private //Attribute
    FDoorOpen : boolean;
    FLightOn : boolean;
    FHeight : integer;
    FR : integer;
    FWidth : integer;
    FX : integer;
    FY : integer;
    FContainer : TCanvas;
    FRoofColor : TColor;
    FWallColor : TColor;

  private //Methoden
    procedure Delete; virtual;
    procedure DrawDoor; virtual;
    procedure DrawRoof; virtual;
    procedure DrawWall; virtual;
    procedure DrawWindow; virtual;

  public //Methoden
    constructor Create (x: integer; y: integer; w: integer; rc: tColor; wc: TColor; Container: TCanvas); virtual;
    procedure CloseDoor; virtual;
    procedure DrawHouse; virtual;
    procedure DrawNew (x: integer; y: integer; w: integer; rc: tColor; wc: TColor); virtual;
    procedure LightOff; virtual;
    procedure LightOn; virtual;
    procedure OpenDoor; virtual;
    function IsDoorOpen : boolean; virtual;
    function IsLightOn : boolean; virtual;
    destructor Destroy; virtual;

   end;

implementation


x, y und w sind Parameter für das Haus, rc und wc Dach- bzw. Wandfarbe. Das Haus wird mit DrawHouse auf der gewünschten Zeichenfläche gezeichnet. Im Grunde müsste ich beim Zerstören dann auch dafür sorgen, dass das Haus von der Zeichenfläche verschwindet. Nur müsste - meinem Verständnis nach - das Objekt Haus nach dem Aufruf von destroy nicht mehr verfügbar sein, d. h. ein DrawNew müsste z. B. zu einer Fehlermeldung führen ... aber das tuts nicht. Hab ich da ein fundamentales Verständnisproblem?


Marc. - So 26.10.08 14:34

Es wundert mich gerade selbst ein wenig, dass es nicht funktioniert. :nixweiss:
Was aber funktionieren sollte, wäre FreeAndNil(Object); anstelle von Destroy.

€: Was aber scheinbar auch nicht der Fall ist. Lediglich die Referenz wird NIL gesetzt, das Objekt bleibt bestehen...

Grüße,
Marc


alzaimar - So 26.10.08 14:42

Du solltest vielleicht den Destruktor als 'override' deklarieren.


Marc. - So 26.10.08 14:56

user profile iconalzaimar hat folgendes geschrieben Zum zitierten Posting springen:
Du solltest vielleicht den Destruktor als 'override' deklarieren.

Daran habe ich bereits auch gedacht, dennoch funktioniert folgendes Konstrukt nicht so, wie es soll:

Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
type
  TMyClass = class
  public
     procedure Test;
     destructor Destroy; override;
  end;
...
destructor TMyClass.Destroy;
begin
  inherited Destroy;
end;
...
  with TMyClass.Create do
   try
     Test;
   finally
      Free;
      Test; // Wird ohne Fehler aufgerufen
   end;

Oder bin ich heute ganz neben der Kappe? :gruebel:


Hidden - So 26.10.08 15:19

Wie sieht denn die Prozedur 'Test' aus? les' oder schreib' mal Variablen.. Dürfte aber eigentlich schon so nicht gehen.

Vielleicht optimiert der Compiler das Objekt weg? :gruebel: Kann ich mir aber eignetlich auch nicht vorstellen.


Marc. - So 26.10.08 15:25

user profile iconHidden hat folgendes geschrieben Zum zitierten Posting springen:
Wie sieht denn die Prozedur 'Test' aus? les' oder schreib' mal Variablen.. Dürfte aber eigentlich schon so nicht gehen.

Aus Abstraktionsgründen, habe ich diese nicht mit ins Beispiel genommen. Beim eigentlichen Test habe ich in der Klasse TMyClass noch eine Feldvariable namens fZahl deklariert.
Die Prozedur Test sieht dann folgendermaßen aus:

Delphi-Quelltext
1:
2:
3:
4:
5:
procedure TMyclass.Test;
begin
  fZahl := 3;
  ShowMessage(IntToStr(fZahl));
end;


IHops - So 26.10.08 17:28

Na, da bin ich erstmal beruhigt, dass ich es nicht als einziger nicht verstehe :roll: ... trotzdem vielen Dank schon mal für eure Mühe.

Hab mal weiter gesucht ...

Delphi-Quelltext
1:
2:
3:
destructor TObject.Destroy;
begin
end;

... so sieht der Destruktor des TObject in Delphi aus ... das erklärt zumindest, dass nix passiert. Allerdings kann ich nicht verstehen, weshalb mit Destroy Objekte nicht gelöscht werden können ... :?

In der Hilfe steht dazu: "Rufen Sie System::TObject::Destroy nicht direkt auf. Verwenden Sie stattdessen System::TObject::Free. Die Methode Free überprüft, ob die Objekt-Referenz nicht bereits nil ist und ruft System::TObject::Destroy nur bei Bedarf auf." Nun ja, das hab ich gemacht - mit dem Ergebnis, dass das Programm nun gleich beim Aufruf von Free abstürzt, obwohl ich ein try ... except verwende und das Free das eigentlich auch vorher abtesten dürfte:


Delphi-Quelltext
1:
2:
3:
4:
5:
procedure TObject.Free;
begin
  if Self <> nil then
    Destroy;
end;


alzaimar - So 26.10.08 17:34

Es ist vollkommen normal, das du eine Methode aufrufen kannst, nachdem das Objekt freigegeben wurde, warum auch nicht. Der Code existiert ja unabhängig vom Objekt. Du bekommst erst dann Probleme, wenn Du innerhalb der Methode auf Klassenfelder zugreifst, weil dann dieser Speicherbereich anderweitig vergeben sein könnte.


Hidden - So 26.10.08 18:06

Hi,

.Free hat einen Sicherungsschalter, falls das Objekt nicht existiert, das hast du schon richtig erkannt. Dieser funktioniert allerdings nur, wenn das Objekt nil ist(das Speicherreferenz-Symbol für "leer").

Deshalb sollten Objekte nur mit FreeAndNil(MyObject) freigegeben werden. Ferner muss eine Variable initialisiert werden. Das heißt, bei der Initialisierung deiner Anwedung solltest du einen Wert hineinschreiben. Entweder ist das z.B. MyInteger := 0; bei gewöhnlichen Variablen(deren Freigabe du nicht selbst übernehmen musst), MyReference := TMyClass.Create(Params); oder, falls du es nicht direkt erzeugen willst, MyReference := nil;.

Zu try: Das liegt an der IDE(bei dir Delphi?). Die Fehlermeldungen treten auf und werden programmintern behandelt. Delphi erkennt sie aber isoliert nocheinmal und sorgt dafür, dass sie dir trotzdem angezeigt werden. Und das ist auch gut so. Das ignorieren von Fehlermeldungen ist nämlich so ziemlich des Pudels Kern, wenn ich das mal so sagen darf ;)

mfG,

Moderiert von user profile iconmatze: Delphi-Tags hinzugefügt


IHops - So 26.10.08 19:17

Hm, das mit dem Haus:= nil hab ich ja dann auch gemacht ... damit geht es auch. Nach meinem Verständnis sollte aber gerade die Methode Destroy dieses alles erledigen ... zumindest hatte ich das mal so gelernt. Und nun sehe ich, dass Destroy eigentlich gar nichts mehr macht ...


Hidden - So 26.10.08 19:36

Hi,

Das ist so nicht ganz richtig. Die eigentliche Freigabe des Speichers geschieht über Destroy(das steht da nur nicht drin, sondern macht imho der Compiler automatisch. Deshalb wird die Methode ja gesondert bezeichnet - "destructor".

Free ruft ja auch nur destroy auf, insofern wird destroy nie verwendet. Es wird immer nur geFreet, jede Verwendung ist implizit.

Das mit dem Nil hat aber garnichts mit dem Objekt zu tun, sondern mit der Objektreferenz. Das ist praktisch nur ein Integer-Wert, in dem steht, wo das Objekt zu finden ist.

Deshalb kann FreeAndNil auch keine Methode des objekts sein - es hat garncihts mit ihm zu tun. Sonst würde das wahrscheinlich schon mit .Free erledigt.

mfG,


IHops - So 26.10.08 21:09

Wenn ich mal zu einer Schlussfolgerung kommen soll:

(1)
Haus.Destroy benötige ich nur, um das Haus von der Bildfläche zu löschen (in diesem Fall ein rein optisches Problem) ...

(2)
... rufe Haus.Destroy aber nie auf, sondern Haus.Free, damit wird der Destructor (bei Bedarf) automatisch aufgerufen.

(3)
Um das Haus komplett rauszuwerfen, setze ich die Referenz mit Haus:= nil ins Nirvana ...


delfiphan - So 26.10.08 21:25

Die Unterscheidung (1) und (3) gibt es nicht. Es gibt nicht verschiedene Arten von Freigaben. Eine für das "Löschen von der Bildfläche" und die andere fürs "komplette Rauswerfen". Entweder ist das Objekt da, oder eben nicht. Solange es existiert, darfst du darauf zugreifen. Sobald es freigegeben ist, nicht mehr. Es kann eine Hilfe sein, die Referenz nach der Freigabe auf nil zu setzen. Dann kriegst du bei einem allfälligen Zugriff auf Felder eine Fehlermeldung, und ein erneuter Freigabeversuch über Free wird ignoriert.

Dass du nach einem Free oder Destroy immer noch Methoden des Objekts aufrufen kannst, ist nicht verwunderlich. Eine Methode ist vergleichbar mit einer Prozedur, die "this" als ersten Parameter hat. Ob "this" noch gültig ist oder nil, ist für den Aufruf egal. Erst wenn du darauf zugreifst gibt es Probleme, weil der Speicherblock entweder schon freigegeben wurde, anderweitig verwendet wird, oder eben nil ist. Kurz: Ein Zugriff aufs Objekt nach dem Freigeben ist ungültig, wird aber nicht zwingend mit einer Fehlermeldung quittiert. Du musst den Code selbst so gestalten, dass ein solcher Zugriff nie passiert.


Yogu - So 26.10.08 22:29

Wie kommt es eigentlich, dass die Zeichenmehtode immer noch aufgerufen wurde, obwohl du das Objekt eigentlich löschen willst? So wie deine Klassendeklaration aussieht (ist ja nicht von TControl abgeleitet), rufst du die Methode DrawHouse manuell auf. Vor diesen Aufruf sollte doch noch eine Methode, die überprüft, ob überhaupt ein Haus existiert.

Wie sieht denn dieser Aufruf aus? Poste doch mal etwas Quelltext um diesen Bereich.


IHops - Mo 27.10.08 10:04

Also ein Haus wird durch Klick auf einen Button erstellt und auf die gewünschte Zeichenfläche gezeichnet:

Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
procedure TFrmMain.BtnCreateHouseClick(Sender: TObject);
var
  x, y, b : integer;
  rc, wc : TColor;
begin
  GetParameter(x,y,b,rc,wc);
  Haus:= THaus.Create(x,y,b,rc,wc,FrmMain.Canvas);
  Haus.DrawHouse;
end;

...

constructor THaus.Create (x: integer; y: integer; w: integer; rc: tColor; wc: TColor; Container: TCanvas);
begin
  inherited Create;
  FX := x;
  FY := y;
  FWidth := w;
  FR := FWidth div 5;
  FHeight := FWidth;
  FLightOn := false;
  FDoorOpen := false;
  FRoofColor := rc;
  FWallColor := wc;
  FContainer := Container;
end;


@delfiphan: Das es keine Unterscheidung zwischen (1) und (3) gibt war ja auch meine (ursprünglich) Meinung, nur sorgt das Destroy eben nicht dafür, dass auch die Referenz entfernt wird; der Zeiger Haus weist immer noch auf die korrekte Stelle im Speicher. So passiert es mir halt, dass auch nach einem Aufruf von Destroy noch gezeichnet werden kann, obwohl es nicht mehr da ist. Eine mögliche Erklärung kann sein, dass der Speicher nicht wirklich freigegeben bzw. anderweitig verwendet wird, so dass an der entsprechenden Adresse halt immer noch "brauchbare" Daten stehen.

Schlussfolgerung: Ich hab meine Objekte (bisher) nur unzureichend freigegeben ... :oops: ... muss es halt in Zukunft sorgfältiger tun :idea:


turboPASCAL - Mo 27.10.08 10:13

Wie oft erstellst du denn das Haus ? Mehrmals ?

Kannst du mal den gesammten Quelltext (als Zip) anhängen ?


delfiphan - Mo 27.10.08 11:50

user profile iconIHops hat folgendes geschrieben Zum zitierten Posting springen:
...der Zeiger Haus weist immer noch auf die korrekte Stelle im Speicher. So passiert es mir halt, dass auch nach einem Aufruf von Destroy noch gezeichnet werden kann, obwohl es nicht mehr da ist. Eine mögliche Erklärung kann sein, dass der Speicher nicht wirklich freigegeben bzw. anderweitig verwendet wird, so dass an der entsprechenden Adresse halt immer noch "brauchbare" Daten stehen.

Ich glaube du meintest das richtige, hat aber evtl. zu Missverständnissen geführt. Ich würde nicht sagen, dass "Haus" auf die "korrekte" Stelle im Speicher zeigt, sondern auf die gleiche(, aber ungültig gewordene) Stelle.

Deine Erklärung ist richtig. Bei der Freigabe kleiner Speicherblöcke passiert beim Delphi Memory Manager mit dem eigentlichen Speicherblock meistens nichts. Sondern es wird lediglich die Buchhaltung angepasst, dass die Stelle jetzt frei ist und wieder belegt werden kann. Dabei wird die Stelle selbst meist weder physikalisch freigegeben noch geräumt noch gelöscht, sodass ein Zugriff nicht zwingend zu einem Fehler führt. Die Stelle könnte aber schon im nächsten Augenblick mit etwas anderem überschrieben oder physikalisch freigegeben werden.


IHops - Mo 27.10.08 19:16

user profile iconturboPASCAL hat folgendes geschrieben Zum zitierten Posting springen:
Wie oft erstellst du denn das Haus ? Mehrmals ?

Das Haus wird nur 1x erstellt, d.h. Haus.Create wird nur 1x aufgerufen (zumindest ist es so gedacht - wird aber programmtechnisch nicht konkret erzwungen). Mit Haus.Draw kann es nur beliebig oft gezeichnet werden.


Delete - Mo 27.10.08 19:44

Zitat:

Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
procedure TFrmMain.BtnDestroyHouseClick(Sender: TObject);
begin
  try
    Haus.Destroy;
    Haus := nil;
    FrmHouse.Hide;
  except
    ShowMessage('Haus kann nicht entfernt werden, da nicht mehr da.');
  end;
end;


Destroy sollte niemals direkt aufgerufen werden.

Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
procedure TFrmMain.BtnDestroyHouseClick(Sender: TObject);
begin
  try
    FreeAndNil(Haus);
    FrmHouse.Hide;
  except
    ShowMessage('Haus kann nicht entfernt werden, da nicht mehr da.'); //sollte eigentlich nie eintreten
  end;
end;


turboPASCAL - Mo 27.10.08 20:09

Jupp, so wirds dann was.

Noch etwas:

Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
procedure TFrmMain.Change(Sender: TObject);
var
  x, y, w: integer;
  rc, wc: TColor;
begin
  try
    GetParameter(x,y,w,rc,wc);
    if Assigned(Haus) then       //  <--<<
      Haus.DrawNew(x,y,w,rc,wc);
  except
    ShowMessage('Haus wurde noch nicht erstellt.');
  end;
end;


Du solltest prüfen ob das Haus da ist bevor du es zeichnest. ;)


IHops - Di 28.10.08 00:53

Hm, das FreeAndNil ist mir neu, funktioniert aber und scheint wirklich die Lösung zu sein. Hab grad nochmal in einigen (2) Büchern nachgesehen ... dort wird das Destroy immer aufgerufen - nunja, man lernt nie aus.

Die Überprüfung, ob das Objekt Haus noch da ist möglich. Genügt aber nicht die Absicherung mit try..except. Das hat auch funktioniert. Den Test mit Assigned(Haus) müsste ich doch dann immer einsetzen, oder macht er an dieser Stelle besonders Sinn?


Yogu - Di 28.10.08 01:07

user profile iconIHops hat folgendes geschrieben Zum zitierten Posting springen:
Die Überprüfung, ob das Objekt Haus noch da ist möglich. Genügt aber nicht die Absicherung mit try..except.

try ... except ist nur für einen einzigen Zweck gedacht: Das Umformen von Fehlern in passendere Fehlermeldungen. Du solltest nie den Except-Teil leer lassen. Das ist so, als ob du dein Auto gegen die Wand fahren lässt, nur um zu testen, ob da eine Wand ist. Genauso gut könntest du die Augen aufmachen und nach vorne schauen.

user profile iconIHops hat folgendes geschrieben Zum zitierten Posting springen:
Das hat auch funktioniert.

Tut es. Ist aber sehr unsauber.

user profile iconIHops hat folgendes geschrieben Zum zitierten Posting springen:
Den Test mit Assigned(Haus) müsste ich doch dann immer einsetzen, oder macht er an dieser Stelle besonders Sinn?

Wenn dein Haus tatsächlich manchmal da ist und manchmal nicht - es macht eigentlich immer Sinn, wenn das Haus auch mal weg sein könnte.

Zum Testen kannst du auch mal alle diese Abfragen weglassen, und viel rumspielen. Dann fügst du überall dort Abfragen ein, bei denen Fehler auftreten. Oder du schaust dir einfach den Quelltext an, und überlegst selbst ;)


IHops - Mi 29.10.08 23:44

Vielen Dank für die Hilfe ... hat geholfen.