Zitat von
NerdIII:
Hi, ich stoße beim Entwickeln immer wieder auf Probleme beim Umsetzen von
OOP.
'Objekte da freigeben wo sie erzeugt werden.' heißt es so schön.
Nun sehe ich in Code von anderen (und auch von mir ab und zu) Methoden, die TStringLists oder TBitmaps erzeugen und zurückgeben. Soll nun die Klasse deren Methode ich aufrufe sich einen Pointer auf die Bitmap/Liste merken, damit sie den Speicher auch selbst wieder freigeben kann oder soll die aufrufende Stelle das tun. Und wie kennzeichne ich eine Methode am besten, damit klar wird, dass jemand der sie verwendet noch Speicher freigeben muss?
(Es gibt Beispiele, z.B. bei Formatierungsfunktionen in C, die den Rückgabewert selber verwalten und freigeben. Das macht das Entwickeln angenehm sorglos.)
Methoden, die ein Objekt erzeugen und an den Aufrufer zurückgeben, so dass der Aufrufer für die Freigabe des Objekts verantwortlich ist, heissen bei mir immer
Create...
So ist beim Methodenaufruf sofort ersichtlich, dass ein Objekt erzeugt wird.
Für komplett eigene Klassen benutze ich Referenzzählung zur Speicherverwaltung. Ich habe eine Klasse TCountedObject, die TObject um eine Zählervariable erweitert. TCountedObject besitzt die Methoden Retain, die den Referenzzähler um 1 erhöht und Release, die den Zähler um 1 dekrementiert und Free aufruft, wenn der Zähler 0 erreicht. Ausserdem gibt es noch die Methode AutoRelease, die das Objekt für einen späteren Aufruf von Release vormerkt. Dafür gibt es noch die Singleton-Klasse TAutoReleasePool, die eine Liste von TCountedObjects verwaltet. Der Aufruf von AutoRelease fügt das Objekt dieser Liste hinzu. Im OnIdle-Event von Application wird dann Release für alle Objekte im AutoReleasePool aufgerufen.
So kann eine Methode ein TCountedObject erzeugen, sofort AutoRelease dafür aufrufen und das Objekt dann an den Aufrufer zurückgeben. Will der Aufrufer das Objekt nicht über die aktuelle Methode hinaus verwenden, braucht er gar nichts zu tun - sobald die aktuelle Ereignisverarbeitung abgeschlossen ist, wird im Idle-Event der Anwendung für das Objekt Release aufgerufen, was den Referenzzähler auf 0 dekrementiert und das Objekt damit freigibt. Will der Aufrufer das Objekt länger behalten, ruft er Retain auf. Damit überlebt das Objekt den Release-Aufruf im OnIdle. Weil der Aufrufer Retain aufgerufen hat, ist er dann auch für die Freigabe mit Release verantwortlich.
Dieser Mechanismus wirkt erstmal recht kompliziert, aber er sorgt dafür, das man normalerweise eine korrekte Speicherverwaltung durch Befolgen von wenigen einfachen Regeln erhält:
1. Ein Objekt, das ein anderes Objekt erzeugt (durch direkten Aufruf des Konstruktors oder durch eine Funktion namens CreateXYZ), ist dafür verantwortlich, durch Aufruf von Release für die Freigabe zu sorgen
2. Ein Objekt, das eine Referenz auf anderes Objekt dauerhaft speichert (also in einer nicht lokalen Variablen), muss für dieses Objekt Retain aufrufen. Danach ist es auch für den Aufruf von Release verantwortlich, als hätte es das Objekt selbst erzeugt.
3. Eine Funktion, die ein Objekt erzeugt und an den Aufrufer zurückgibt, ohne eine Referenz auf das Objekt zu behalten, kann statt Release AutoRelease aufrufen und kommt damit ihrer Verpflichtung, Release aufzurufen, nach und kann das Objekt trotzdem noch zurückgeben.
Problematisch sind dann nur noch Strukturen, bei denen Objekte gegenseitige Referenzen speichern. Hier würde dieses Verfahren zu einem Speicherleck führen, wenn sich die beiden Objekte gegenseitig Retainen. Normalerweise ist es dann sinnvoll, eine Hierarchie zu definieren, in der eins der Objekte dem anderen übergeordnet ist. Das übergeordnete Objekt ruft dann Retain und Release für das untergeordnete auf, aber nicht umgekehrt.
Um Objekte mit Referenzzählung von solchen ohne Referenzzählung (die gesamte Delphi-Standardbibliothek) zu unterscheiden, verwende ich für von TCountedObject abgleitete Klassen immer das Namenspräfix TC.
Stefan