Diskussion:Ressourcenbelegung ist Initialisierung
Verständlichkeit
BearbeitenIch glaube, ein kleines Code-Beispiel wäre nicht schlecht. --jpp ?! 23:42, 6. Jun 2006 (CEST)
müsste es in ", wohingegen Boost.SmartPtr und Boost.PointerContainer eine ganze Reihe Hilfsmittel zur automatischen Verwaltung von Variablen auf dem Stack bereithalten." nicht heap heissen? Also
", wohingegen Boost.SmartPtr und Boost.PointerContainer eine ganze Reihe Hilfsmittel zur automatischen Verwaltung von Variablen auf dem Heap bereithalten."
Beispiel
BearbeitenDas Beispiel ist nicht wirklich gut. Erstens wird es bereits durch std::ofstream implementiert, zweitens wird die Klasseninvariante "Datei ist während der gesamten Lebenszeit der Klasse geöffnet" nicht implementiert, drittens wird die Regel der großen Drei nicht beachtet. --Phst 10:10, 16. Aug. 2010 (CEST)
- Berechtigter Einwand. Aber welches Beispiel wäre denn für RAII deiner Meinung nach besser geeignet? --RokerHRO 21:32, 4. Feb. 2012 (CET)
- Gerade bei nicht kopierbaren Objekten wie Dateihandles ist die Dreierregel nicht sinnvoll anzuwenden, da der Destruktor einer Kopie (shallow oder deep, das ist egal) auch die Quelle zerstört (Datei schließt). Objekte dürfen nur kopier- oder zuweisbar sein, wenn deren interne Resoucen auch vervielfältigbar sind (siehe Mißbrauch von auto_ptr). Das Beispiel ist aber aus anderen Gründen nur halbgut.--46.115.107.186 17:58, 31. Jan. 2013 (CET)
Thema verfehlt
BearbeitenDer Artikel beschäftigt sich überwiegend mit der Ressourcenfreigabe und nicht mit der zu fordernden Initialisierung. Nach meinem seit 20 Jahren praktizierten Verständnis von RAII ist die automatische Freigabe nur ein unbedeutender Nebeneffekt des Konstruktor/Destruktor-Konzepts. Das Konzept ist auch als "Declaration is Initialization" bekannt.
- 1. Ressourcenbelegung/Deklaration erst, wenn das Objekt auch gebraucht wird und alle Informationen zur Initialisierung auch bekannt sind (zB Dateiname oder Bildgröße)
- 2. Keine Reservierung/Deklaration leerer Objekte auf Vorrat (zB im Dekalrationsblock am Anfang einer Funktion nach C-, Basic-, Pascal- oder Fortran-Konvention, auch Java), es sei denn, daß auch ein leeres Objekt sinnvoll und fehlerfrei benutzt werden kann (zB ein leerer String oder Integer, aber eine nicht initialisierte Fleißkommazahl kann auch ungültig (NaN) sein) und es gute Gründe dafür gibt
- 3. Möglichst kleiner Gültigkeitbereich (Scope) eines Objekts, es sei denn, es wäre wohlüberlegt (zB Pufferobjekt für eine Schleife einmalig außerhalb anlegen)
- 4.a die Existenz und Erreichbarkeit eines Objekts impliziert seine Gültigkeit
- 4.b nach der Initialisierung muß jeder bestimmungsgemäße Zugriff oder Aufruf mit zulässigen Parametern auch ein gültiges Ergebnis liefern und die Konsistenz des Objekts erhalten (vergleiche analog BIBO-Stabilität, "Valid-In-Valid-Out")
- 4.c Gültigkeitsüberprüfungen innerhalb des erfolgreich angelegten Objekts sind sinnlos ("Bin ich gültig? Lebe ich?", die Antwort "Nein" ist paradox oder wenigstens inkonsistent, Gödelscher Unvollständigkeitssatz, "Alle Kretaer sind Lügner ... sagt der Kretaer"), außer beim Destruktor, damit ein (teilweise) ungültig initialisiertes Objekt die bereits erfolgreich belegten Ressourcen wieder freigeben kann
- 4.d während der Debug-Phase dürfen/müssen die übergebenen Parameter sehr wohl überprüft werden; ebenso darf/muß geprüft werden, ob formal gültige Eingaben das Objekt ungültig, unbestimmt oder widersprüchlich machen (zB Zählerüberlauf, Kreisradius unendlich, Dreieck ohne Seiten, dessen Innenwinkelsumme nicht bestimmt werden kann)
- 5. Standardkonstruktoren nur, wenn ein Mißbrauch ausgeschlossen ist (zB Zugriffsfunktion auf eine nicht existierende Datei); sonst Standardkonstruktor "verstecken"; gleiches gilt für den Copy-Konstruktor und den Standardzuweisungsoperator (shallow copy)
- 6. Ein Objekt muß so konstruiert sein, daß die belegten Ressourcen während seiner Erreichbarkeit nicht ungültig werden können (zB weil es Seiteneffekte gibt, ein anderes Objekt die Kapselung umgehen kann, im ärgerlichsten Fall, daß eine Kopie des Objektes sich selbst löscht und dabei die Ressourcen der Quelle löscht)
Das ist meine Quintessenz aus RAII, die ich aus den Werken mehrerer Autoren so zusammengeführt habe. Wichtigste Quelle ist natürlich "The C++ Programming Language" von Bjarne Stroustrup, insbesondere für die ersten drei Regeln. Regel vier entspringt dem allgemeinen Grundsatz, daß der Benutzer eines Objekts für dessen sinnvollen Gebrauch selbst verantwortlich ist und daß der Objektentwickler nur ein konsistentes Objekt zu liefern hat. Regel 5 und 6 berücksicht neuere Spitzfindigkeiten, wie sie sich um die Intelligenten Zeiger (auto_ptr, unique_ptr) entwickeln, mit der Forderung: "Diese Schweinerei muß eine andere werden!" Der WebLink auf auto_ptr bezieht sich daher auf eine veraltete, oder wenigstens kritisierte Technik aus den Anfängen des RAII.
Ich ändere zunächst einmal den Beispiel-Code.--46.115.107.186 17:58, 31. Jan. 2013 (CET)
- Übrigens: die Delphi Variante von Pascal kann seit V10.3.3 auch inline variablen, man muss diese also nicht mehr zwingend im Deklarationsblock der Methode/Prozedur/Funktion deklarieren. --2A02:8070:6394:7A00:40FF:22CA:CF2F:5CBE 22:06, 10. Mär. 2022 (CET)
Abschnitt "Varianten" (vorher "Alternativen")
BearbeitenDer Abschnitt muss überarbeitet werden, denn RAII ist auch mit Garbage Collector möglich. --Casu17 (Diskussion) 09:06, 16. Mai 2013 (CEST)
- Das Problem entsteht dann, wenn die GC den Speicher nebenläufig aufräumt. Eine Erwähnung der Nebenläufigkeit, sollte die Begründung richtigstellen. --Plankton314 (Diskussion) 10:33, 16. Mai 2013 (CEST)
- Hab nochmal kurz recherchiert und auf die schnelle keine Sprache mit GC gefunden, die eine deterministische Objektfreigabe garantiert.
- Ich werde die Tage mal einen Blick in die allgemeine GC-Literatur werfen, mir wäre es zumindest neu, dass RAII mit GC überhaupt möglich ist. --Plankton314 (Diskussion) 12:47, 16. Mai 2013 (CEST)
- RAII ist mit GC möglich, egal ob nebenläufig oder nicht. Zum Thema GC siehe Die C++ Programmiersprache, 1.-4. Auflage von Stroustrup. --Casu17 (Diskussion) 12:53, 16. Mai 2013 (CEST)
- Ist diese Aussage in Stroustrups Buch zu finden? Falls ja, wo?
- Das Funktionieren von RAII hängt wesentlich davon ab, dass der Destruktor am Ende des Gültigkeitsbereichs aufgerufen wird. Das ist bei keiner GC-unterstützten Sprache der Fall; weswegen das dortige Destuktur-Äquivalent auch Finalisierer genannt wird.
- Die Aussage RAII sei mit GC möglich, ist in allgemeiner Form definitiv falsch. --Plankton314 (Diskussion) 14:37, 16. Mai 2013 (CEST)
- Stroustrup schreibt in seinem Buch etwas über GC und Destruktoren. (Schau bei Amazon nach. Dort gibt es eine Suchfunktion, über die du die Stelle im Buch bestimmt finden kannst.) Daraus wird klar, wie das Ganze funktioniert. Übrigens haben C++, Ada und D eine GC. C++ und Ada optional und D per Default. Alle 3 Sprachen bieten RAII. Das sollte dir eigentlich zu denken geben. --176.5.171.198 16:10, 16. Mai 2013 (CEST)
- Danke, nicht nötig, ich habe eine Ausgabe des Buchs. Mir ist auch klar, wie das ganze funktioniert - und das Funktionieren in C++ ist durch den Standard gewährleistet.
- Die Behauptung dagegen, dass C++ eine GC hätte ist schlichtweg falsch, da das von der konkreten Implementierung abhängt, zumal der Standard hier nichts vorschreibt.
- In Sprachen, die solch ein Verhalten (konkret: §10.4.4, Zerstörung des Objektes beim Verlassen des Gültigkeitsbereichs) nicht garantieren, kann es auch kein funktionierendes RAII geben - egal, ob mit oder ohne GC.
- Vllt. wäre das (der letzte Satz) eine formal korrekte Darstellung. --Plankton314 (Diskussion) 18:45, 16. Mai 2013 (CEST)
- Ich finde den Satz gar nicht schlecht. Aber ob man ihn als "formal korrekt" bezeichnen kann, weiß ich nicht. Bei Informatik-Themen gibt es manchmal das Problem, dass Begriffe keine völlig klare Definition haben. Ich glaube, dass manche z.B. das, was du im Abschnitt "Alternativen" für C# und Java beschrieben hast, auch als RAII bezeichnen. Tatsächlich hat in C# Dispose die Funktion eines Destruktors. Wird von so einer C#-Klasse eine Klasse in C++/CLI abgeleitet, dann ist Dispose aus Sicht von C++ der Destruktor der Basisklasse. Und auch ein Using-Abschnitt hat ja einen Gültigkeitsbereich, so dass man auch da sagen kann, beim Verlassen des Gültigkeitsbereiches wird der Destruktor aufgerufen. Analog bei Java. Vielleicht sollte man den Abschnitt umbenennen von "Alternativen" in "Varianten". --Casu17 (Diskussion) 13:58, 17. Mai 2013 (CEST)
- Umgesetzt. --Plankton314 (Diskussion) 14:27, 18. Mai 2013 (CEST)
- In Delphi kann man übrigens eine automatische Freigabe bei Verlassen des Scopes erreichen, in dem man der Klasse ein Interface spendiert und nur mit der Interface Referenz arbeitet. Dann wird das Referenzgezählt und wenn der Rweferenzzähler 0 erreicht, wird das freigegeben. Würde mal sagen, dass das ziemlich deterministisch ist... --2A02:8070:6394:7A00:40FF:22CA:CF2F:5CBE 22:10, 10. Mär. 2022 (CET)
Noch mal zur GC: Das Verfahren von RAII, wie es in C++ umgesetzt ist (soweit ich weiß, ist es so auch in D und anderen), basiert auf den Mechanismen zur Verwaltung lokaler Variablen der Speicherklasse "automatisch", und das heißt, eine GC kann keine Rolle spielen, da eine GC eine andere Speicherklasse ist. Man kann sich das am Programmierbeispiel im Artikel klar machen. Wo sollte da eine GC zum Zuge kommen? Ob irgendwo im Hintergrund eine GC betrieben wird oder nicht, ist also vollkommen egal. Das Verfahren RAII bleibt davon unberührt. --Casu17 (Diskussion) 07:29, 18. Mai 2013 (CEST)
- Ich bin mir gerade nicht sicher, ob ich dich missverstehe oder du die GC :)
- Nur wenn zB. lokale Variablen/Objekte, wie in C++, bei Deklaration erstellt und beim Verlassen des Gültigkeitsbereichs wieder automatisch zerstört werden, ist das noch keine GC. Eine GC läuft i.A. im Hintergrund und räumt nur in gewissen Abständen auf - oder wenn es für ein neues Objekt wegen mangelndem Speicher zwingend nötig ist. Mir ist gerade auch keine GC bekannt, die nicht nebenläufig wäre. Die Idee bei der GC war es ja, die Speicherfreigabe - vor allem vieler kleiner Bereiche - zu sammeln und zu einem gemeinsamen Zeitpunkt stattfinden zu lassen und im Anschluss den Speicher zu kompaktieren.
- Und das wiederum lässt dann solche Probleme wie in C# und Java entstehen, für die es dann eine spezielle Variante braucht, um die Ressourcenfreigabe zu einem definierten Zeitpunkt stattfinden zu lassen - und so RAII dennoch irgendwie zu ermöglichen.
- Das ist auch nicht direkt die "Schuld" der GC, sondern der Sprach-Specs, die hier eben (vllt. wegen der GC, man weiß es nicht) keine so strenge Anforderungen an die Objektlebensdauer stellen.
- Ich verstehe leider auch deine Frage nicht so wirklich, wo in dem Beispiel im Artikel GC zum Zuge kommen sollte. Natürlich irgendwann nach dem Verlassen der main-Funktion. (Gut, das mag ein Sonderfall sein, weil das Programm dann endet - aber grundsätzlich immer dann, wenn das Objekt wieder freigegeben werden soll, also nach der }-Klammer.) --Plankton314 (Diskussion) 14:03, 18. Mai 2013 (CEST)
- Bei der Programmiersprache C++ ist es möglich, optional einen Garbage Collector zu betreiben. Du kannst dir nun das Programmierbeispiel aus dem Artikel so vorstellen, als würde im Hintergrund der GC laufen. Wichtig für das Verständnis ist dann die Frage, wie der vom Objekt namens datei belegte Speicher freigegeben wird. Wird er a) durch den GC freigegeben oder b) nicht durch den GC freigegeben? Die richtige Antwort ist b. Damit kann man sich klar machen, dass ein ggf. vorhandener GC die Funktion von RAII nicht beeinträchtigt.
- Auch für C# und Java bereitet der GC keine Probleme im Hinblick auf RAII. In C# ließe sich darüber hinaus die Syntax ganz ähnlich realisieren wie in C++/CLI. Man hat es nur einfach nicht so gemacht. C++/CLI ist erst später entstanden als C#, sonst wäre es vielleicht anders gekommen. --Casu17 (Diskussion) 10:48, 19. Mai 2013 (CEST)
- Was verleitet dich zu der Annahme, dass das Objekt datei gerade nicht durch eine GC freigegeben werden würde?
- Leider kann ich auch deiner Aussage "Auch für C# und Java bereitet der GC keine Probleme im Hinblick auf RAII" nicht folgen. Genau das ist doch der Fall und deswegen gibt es diese using- bzw. try-with-Konstrukte. --Plankton314 (Diskussion) 11:18, 19. Mai 2013 (CEST)
- Ich habe mir schon gedacht, dass du diesen Punkt missverstanden hast. Du solltest dich noch etwas mehr in die Thematik GC einarbeiten, andernfalls ist es nicht sinnvoll, Artikel zu dem Thema zu bearbeiten. Wenn du nichts dagegen hast, nehme ich die Aussage, in bestimmten Sprachen sei die Technik aufgrund des Garbage Collectors nicht möglich, aus dem Artikel. --Casu17 (Diskussion) 11:03, 27. Mai 2013 (CEST)
- Es ist die Arbeitsweise des GC oder auf einer abstrakteren Ebene die laxere Spezifizierung der Objektlebensdauer. Diese Aussage entstammt der aufgeführten Literatur.
- Nun finden sich natürlich auch in Fachliteratur immer wieder Fehler bzw. unscharfe oder zu spezielle Aussagen. Wenn du diese Aussage dennoch entfernen möchtest, lege dies bitte zumindest argumentativ dar oder belege es. Ein abstrakt Verweis, der Andere solle sich in das Thema einarbeiten, begründet noch nichts. --Plankton314 (Diskussion) 12:45, 27. Mai 2013 (CEST)
- Im Raum steht die Aussage "In C# oder Java ist diese Technik dagegen aufgrund des Garbage Collectors nicht direkt möglich" (steht so im Artikel). Ich kann das leider nicht der aufgeführten Literatur entnehmen. Könntest du bitte die Stelle zitieren, der du das entnimmst?
- Bezüglich der Fehler in der Fachliteratur muss ich dir Recht geben. Deswegen wäre es gut, wenn wir die Stelle, die du meinst, in Augenschein nehmen könnten. Argumentativ dargelegt habe ich aber meiner Meinung nach meinen Standpunkt (Stichwort Speicherklasse). Das Problem ist nur, wenn die Wissenslücken in einem Gebiet zu groß sind, dann hat es auch keinen Zweck. Es kann ja nicht sein, dass jeder Laie seine Standpunkte in Wikipedia-Artikeln so lange lassen darf, bis ihn ein Experte vom Gegenteil überzeugt hat. Und das entspricht eigentlich auch der Arbeitsweise in der Wikipedia; soll heißen, sobald Zweifel bezüglich einer Aussage aufkommen, wird sie entfernt.
- Dass wir alle, je nach Wissensgebiet, abwechselnd mal Laie, mal Experte sind, bleibt in so einer schnelllebigen Materie wie der Informatik nicht aus. Daraus darf man keine Prestige-Frage machen.
- --Casu17 (Diskussion) 18:23, 1. Jun. 2013 (CEST)
- In C# und Java gibt es die Speicherklasse "automatisch", wie in C++, überhaupt nicht. Es existieren grundsätzlich nur Value- und Reference-Variablen, wobei Value-Variablen dem noch am nächsten kommen. Klassenobjekte werden dagegen mittels new instanziert und sind somit Referenz-Variablen die auf den Managed Heap kommen, der wiederum vom GC verwaltet wird.
- Es existieren auch keine Destruktoren, sondern nur Finalisierungsmethoden, die jedoch zu einem unbekannten Zeitpunkt aufgerufen werden. Sie sind deshalb kein echtes Äquivalent zu Destrukturen, da eine Ressourcenfreigabe beim Verlassen des Gültigkeitsbereichs durch sie nicht garantiert werden kann.
- Nachzulesen auf den Seiten 353 unten bzw. 354 mitte in [2].
- Diese bei C++ gegebenen Garantien sind eine fundamentale Annahme für das korrekte Funktionieren von RAII. Durch ihr Fehlen schließen sie RAII in diesen Sprachen explizit aus. Weswegen dort auch die erwähnten Hilfskonstrukte eingeführt wurden. --Plankton314 (Diskussion) 13:30, 3. Jun. 2013 (CEST)
- Das ist genau der Punkt, es gibt in Java eben keine Speicherklasse, die es ermöglicht, den Zeitrpunkt festzulegen, an dem ein Objekt freigegeben und die finalize()-Methode aufgerufen wird, etwa bei Verlassen des Scopes oder durch expliziten Aufruf eines Destruktors. Die einzige Möglichkeit, überhaupt sicherzustellen, dass ein Finalizer vor Beenden der VM aufgerufen wird, ist deprecated, da bei ihrer Verwendung die Konsistenz der Anwendung und ihrer Daten nicht sichergestellt werden kann. Daher ist die finalize-Methode zum Freigeben von Resourcen nicht besonders gut geeignet. Allgemein wird von der Verwendung von Finalizern in Java ganz abgeraten (z.B. http://howtodoinjava.com/2012/10/31/why-not-to-use-finalize-method-in-java/ https://www.securecoding.cert.org/confluence/display/java/MET12-J.+Do+not+use+finalizers).
- Der letzte Satz des Abschnitts ist aber schlichtweg falsch. Mit try-with-resources werden keine Objekte zerstört oder deren Finalizer aufgerufen, es werden nur deren Resourcen freigegeben. Diese Objekte müssen das Interface AutoCloseable implementieren, dessen close()-Methode dann zum Freigeben der Resourcen aufgerufen wird (http://docs.oracle.com/javase/tutorial/essential/exceptions/tryResourceClose.html). Die Objekte selbst existieren aber weiterhin. try-with-resources gibt es erst ab Java 7, davor wurde für diesen Zweck try... finally verwendet und kann natürlich weiterhin verwendet werden.
- Zumindest was Java angeht, ist der Abschnitt imho bis auf den letzten Satz korrekt, mit C# kenne ich mich nicht aus. Es wäre vielleicht sinnvoll, den Sachverhalt für Java richtigzustellen und genauer zu beschreiben und zwei Code-Beispiele hinzuzufügen, eines mit try... finally und eines mit try-with-resources.
- --SchalHorn (Diskussion) 13:38, 10. Sep. 2014 (CEST)
- Vielleicht hilft zur Klärung:
- Damit RAII so funktioniert, wie es gedacht ist, muss sichergestellt sein, dass der Destruktur zu einem definierten Zeitpunkt (in C++ ohne GC ist dieser Zeitpunkt das Verlassen des Skopus oder der Aufruf von
delete
, bei mitnew
erstellten Objekten) aufgerufen wird. Wenn dies nicht garantiert werden kann und Destruktoraufrufe womöglich erst irgendwann viel später oder in „falscher“ Reihenfolge stattfinden (Stichwort „Nichtdeterminismus“), ergibt RAII keinen Sinn, weil der Zeitpunkt der Freigabe der (möglicherweise kritischen) Ressource eben nicht mehr im Programm festgeschrieben ist. - Garbage Collection tut aber genau das: Objekte werden irgendwann und in beliebiger Reihenfolge „zerstört“ (also ihr Speicher freigegeben), wobei dieses „irgendwann“ nach dem Zeitpunkt liegt, an dem die letzte Referenz auf das Objekt aufgegeben wurde. (Sonst wäre es ein sinnloser Garbage Collector.)
- Garbage Collection unternimmt dies jedoch nur für solche Objekte, für deren Verwaltung sie zuständig ist. In sowohl Java als auch C# sind das Objekte, die auf dem Heap angelegt sind. In Java und C# gibt es auch keine anderen Objekte. (
struct
-Instanzen in C# sind keine Objekte, es sei denn, sie sind geboxt. Und dann sind sie auf dem Heap. Siehe unten.) Das ist in C++ anders, weil man Objekte auf dem Stack erstellen kann. Da deren Speicher zwangsläufig spätestens dann freigegeben werden muss, wenn der Stackframe, in dem sie liegen, abgebaut wird, hat man sich beim Design von C++ entschieden, diese Speicherverwaltung komplett deterministisch zu betreiben und an den Skopus der Variablen zu koppeln. - Auf dem Stack abgelegte Objekte (nicht Objektreferenzen!) der Gargabe Collection zu unterwerfen, wäre also inhärent unsinnig. Man müsste ja die GC für jeden Stackframe laufen lassen, der abgebaut wird – was der Idee der GC ja gerade widerspricht.
- Damit RAII so funktioniert, wie es gedacht ist, muss sichergestellt sein, dass der Destruktur zu einem definierten Zeitpunkt (in C++ ohne GC ist dieser Zeitpunkt das Verlassen des Skopus oder der Aufruf von
- Ist RAII mit Garbage Collector also unmöglich? Objekte, die auf dem Heap liegen und von einer Garbage Collection verwaltet werden, können die Anforderungen von RAII definitiv nicht erfüllen. Objekte auf dem Heap, die manuell gelöscht werden, ebenfalls nicht, weil ihr Speicher eben nicht automatisch (z.B. im Exception-Fall) freigegeben wird. Objekte wiederum, die auf dem Stack liegen, können der Garbage Collection überhaupt nicht unterworfen werden; ihre Speicherfreigabe ist immer deterministisch und zumindest insofern automatisch, als dass sie spätestens beim Abbau des Stackframe freigegeben werden.
- Der Knackpunkt hier: ob bei dieser Freigabe auch stets eine gewisse Funktion/Methode aufgerufen wird. In C++ ist das aus naheliegenden Gründen gerade der Destruktor – das muss aber nicht sein (s.u.). In C# haben
struct
s keinen Destruktor, weil sie eben Werte und keine Objekte darstellen sollen. Ein wesentlicher Unterschied von Werten ist, dass sie keine Lebensdauer besitzen. Natürlich wird der Speicherbereich einesint
oder einesRectangle
in C# irgendwann freigegeben (nämlich wenn der Stackframe abgebaut wird), das aber in ein reines Implementationsdetail und mit der intendierten Wertenatur nicht vereinbar. - Schlussfolgerung: RAII und Garbage Collection schließen sich nicht aus – jedenfalls nicht in dem Sinne, dass Sprachen mit GC zwangsläufig kein RAII unterstützen (oder umgekehrt). Jedes konkrete Objekt hingegen kann nur entweder der GC unterliegen oder einem RAII-Mechanismus – nicht beidem. Der Grund, warum Java und C# kein RAII unterstützen, ist nicht, dass sie Garbage Collection besitzen – sondern, dass per Sprachdesign alle ihre Objekte der Garbage Collection unterliegen.
- Was also tun diese Sprachen, um RAII-analoge Ressourcenverwaltung zu ermöglichen? Die grundlegende Antwort ist: Sie trennen Destruktion und Ressourcenfreigabe. Da der Zeitpunkt, zu dem ein Java- oder C#-Objekt zerstört, also sein Speicherbereich freigegeben wird, nicht festgelegt ist und schlimmstenfalls erst beim Programmende erreicht wird, kann der Destruktor (dort Finalizer) also die Aufgabe der Ressourcenfreigabe nicht im RAII-Sinne erfüllen. Deswegen werden Methoden eingeführt, die das stattdessen übernehmen (
Dispose
,close
etc.), und Sprachkonstrukte, mit denen der Aufruf dieser Methoden sichergestellt werden kann (try
–finally
und seit Java 7 „try-with-resources“). Da das Objekt nach dem Aufruf dieser Methoden aber immer noch Speicher belegt („existiert“) und sogar noch referenziert sein kann (und in dem Fall von der GC also nicht gelöscht wird), hat man dann ein Objekt, aber keine Ressource mehr – aus RAII-Sicht ein Objekt in ungültigem Zustand, daher ist das kein klassisches RAII-Muster. - Ich weiß nicht, ob das in der Fachliteratur irgendwo so deutlich steht, aber jedenfalls kann man anhand dieser Überlegungen recht gut einsehen, dass der Satz „In C# oder Java ist RAII aufgrund des Garbage Collectors nicht direkt möglich“ in dieser Form zumindest recht unscharf ist. Genauer wäre: „In C# oder Java ist RAII nicht direkt möglich, da in diesen Sprachen alle Objekte per Garbage Collection freigegeben werden.“ --77.186.50.204 20:36, 15. Okt. 2016 (CEST)
- Vielleicht hilft zur Klärung:
Programmbeispiel
BearbeitenDas Beispiel ist nicht nur schlecht (C-Funktionen, die dank der C++-Standardbibliothek ohnehin nicht gebraucht werden), sondern auch fehlerhaft. Wird eine Instanz vom Typen Datei
kopiert (entweder mit dem impliziten Kopierkonstruktor oder mit dem Zuweisungsoperator), so wird lediglich das Handle kopiert, womit sich beide Instanz auf ein und dieselbe Datei beziehen. Da kein Referenzzähler implementiert ist, kann eine Kopie die originale Instanz ungültig machen. Beispiel:
Datei Datei1("file.txt");
Datei(Datei1); // Temporary wird erzeugt und sogleich wieder zerstört
// Hier ist Datei1 in einem zwar definierten, aber unbrauchbaren Zustand und löst spätestens beim Zerstören einen Fehler aus
Ebenso ist es schleierhaft, weswegen der Typ FILE
ohne vorangehender Namensbereichqualifizierung (std::
) benutzt wurde.
Ich schlage folgende Alternative vor, die auch den Bezug zur Ausnahmesicherheit verdeutlicht:
#include <iostream>
#include <stdexcept>
// Eine Klasse, die ihren Speicher am Ende ihrer Existenz selbst freigibt
template<typename T>
class Ressource
{
T* Zeiger;
// Kopierkonstruktor und Zuweisungsoperator werden private deklariert, damit sie nicht zugänglich sind
Ressource(Ressource const&); // oder mit C++11: = delete
Ressource& operator= (Ressource const&); // oder mit C++11: = delete
public:
explicit Ressource(T* Zeiger) : Zeiger(Zeiger) { }
~Ressource() { delete Zeiger; }
T& Zugriff() const { return *Zeiger; }
};
// Funktion, die u.U. eine Ausnhame auslöst
void f(int Wert)
{
if(Wert == 0)
throw std::invalid_argument("f(int): Wert darf nicht 0 sein");
}
int main()
{
try
{
Ressource<int> SichereDaten(new int(7));
SichereDaten.Zugriff() += 3;
int* UnsichereDaten = new int(5);
*UnsichereDaten *= 3;
f(0);
delete UnsichereDaten; // Wird nie erreicht, ergo wird UnsichereDaten nie freigegeben -> Speicherleck
// SichereDaten wird dank RAII freigegeben, da die Ausnahme von f in diesem Gültigkeitsbereich nicht mehr abgefangen wurde
}
catch(std::invalid_argument const& Ausnahme)
{
std::cout << "Es ist eine Ausnahme aufgetreten:\n\t" << Ausnahme.what();
}
}
RAII vs. Separation of Concerns
BearbeitenWie kann ich mir RAII in größeren, Programmen mit mehreren Modulen/Klassen/Schichten vorstellen? Wenn lt. RAII im selben Gültigkeitsbereich eine Ressource wieder freigegeben werden muss, dann müsste ja z.B. ein Datenzugriffslayer, eine Factory-Method oder eine Repository- oder DAO-Klasse auch für das Löschen der angelegten Objekte zuständig sein. RAII widerspricht mMn den gängigen Prinzipien und Patterns von zumindest OO Programmiersprachen.
Besagt RAII wirklich, dass die Ressourcen im selben Gültigkeitsbereich wieder freigegeben werden müssen, oder doch nur das, was auch im Namen steht, nämlich dass die Ressourcen sofort bei der Belegung initialisiert werden? --Sebastian.Dietrich ✉ 20:46, 28. Apr. 2015 (CEST)
- RAII heißt "Ressourcen im Konstruktor belegen, im Destruktor freigeben". Üblicherweise werden lokale Variablen am Ende des Blocks/Scopes automatisch abgeräumt und dank RAII werden die im Objekt-Konstruktor belegten Ressourcen automatisch wieder freigegeben.
- Hat man Factory-Funktionen, die ein Objekt erzeugen und rausgeben, das eben den Scope der Factory-Funktion überlebt, dann gilt das natürlich auch für die im Objekt gehaltenen Ressourcen: Diese bleiben natürlich weiterhin belegt, so lange eben das Objekt "lebt" und werden mit dem "Ableben" des Objektes automatisch freigegeben.
- Da ich vielleicht etwas C++-betriebsblind bin, ist das für mich leicht verständlich. ^^
- Darum meine Frage: Was genau ist für dich daran nicht oder nur schwer verständlich?
- --RokerHRO (Diskussion) 13:21, 29. Apr. 2015 (CEST)
- Kein Problem - verstehe C++. Du meinst also, dass RAII hat nichts mit dem Gültigkeitsbereich des Erzeugenden zu tun habe, sondern mit dem Gültigkeitsbereich des Erzeugten. D.h. wenn im Konstruktor das Objekte alle Ressourcen anlegt und im Destruktor (spätestens) freigibt, dann entsprichts RAII, auch wenn das delete() von einem anderen Objekt aufgerufen wird als das new()? Oder gar von einem anderen Prozess?
- Wenn dem so ist, dann verstehe ich nicht, warum Garbage Collection dem RAII Prinzip widersprechen sollte - ist ja nur ein anderer Prozess der nebenläufig delete() aufruft, wo dann brav nach RAII alle Ressourcen des Objektes freigegeben werden. Solange das Objekt noch nicht vom Garbage Collector freigegeben wurde ist es halt noch gültig (und auch in Java z.B. über Weak References noch erreichbar).... --Sebastian.Dietrich ✉ 21:37, 29. Apr. 2015 (CEST)
- 1) Objekte rufen nichts auf. Funktionen rufen etwas auf, z.B.
delete
, ohne Klammern übrigens, ist ja keine Funktion, sondern ein Operator. - 2) Es muss schon der gleiche Prozess sein, da verschiedene Prozesse normalerweise voneinander abgeschottete Speicherbereiche haben, somit kann nicht ein Prozess auf die Objekte eines anderen Prozesses zugreifen, und das ist auch gut so.
- 3) Ein Objekt soll verlässlich abgeräumt werden (und damit alle von diesem Objekt belegten Ressourcen freigegegen werden), wenn es nicht mehr gebraucht wird. Und keinen Moment später. Ein Garbage Collector räumt die nicht mehr zugreifbaren Objekte ab, sofern er "Lust dazu hat" (z.B. weil grad CPU-Zeit frei ist) oder weil er muss (weil z.B. der freie Speicher knapp wird). So ein GC-Lauf ist somit nicht deterministisch und das kann für kritische Betriebsmittel sehr von Übel sein.
- 4) Da in modernem C++ die Verwendung von "nackten Zeigern" unüblich geworden ist und inzwischen ziemlich verpönt ist, verringern sich auch die Probleme, die man sich mit "nackten Zeigern" einhandelt, und somit sinkt auch der Bedarf und der Nutzen von einem Garbage Collector. RAII ist und bleibt das leistungsfähigere Konzept. :-)
- --RokerHRO (Diskussion) 19:53, 30. Apr. 2015 (CEST)
- 1) Objekte rufen nichts auf. Funktionen rufen etwas auf, z.B.
- 1) & 2) jaja. 3) verstehe ich nicht. Ein GC ist genaus verlässlich bzw unzuverlässig, wie wenn das Objekt woanders (durch ein Service einer anderen Klasse u.U. in einem anderen Thread) freigegeben wird (oder eben nicht/später z.B. weil der Thread auf Grund hoher Last erst einfach spät dazukommt). Determinismus ist ja eine andere Sache als RAII. 4) verstehe ich auch nicht - was meinst mit "nicht-nackte" Zeiger? Smart-Pointer mit Reference-Counting? Garantiert ja auch nicht, dass ein Objekt aufgeräumt wird (Zyklen). RAII alleine ist auch keine Antwort auf (die höchst komplexe und in vielen Fällen zur Compilezeit nicht lösbare) Frage, wann ein Objekt abgeräumt werden kann.
- Die Frage nochmal anders gestellt: 1) Kann ein nicht-triviales Programm, bei dem Funktionen/Methoden anderer Klassen als der Klasse, die das Objekt erzeugt haben, das Objekt deleten, RAII-konform sein? Wenn dem so ist, dann passt mMn der folgende Satz in der Einleitung nicht: "Die automatische Freigabe wird durch das Verlassen des Gültigkeitsbereichs ausgelöst (am Blockende, bei Ausnahmeauslösung, durch Rückgabe an den Aufrufer, usw.)"
- 2) Kann ein nicht triviales Programm, bei dem zur Compilezeit nicht feststellbar ist, wann ein Objekt abgeräumt werden muss, RAII-konform sein? Wenn ja, wie?
- 3) Was ist der Grund, dass GCs nicht RAII-konform sind? Nebenläufigkeit des GCs oder zeitlich indeterministisches Verhalten des GCs kann es wohl nicht sein (gibts ja in anderen Programmen auch). Dazu hätte ich auch gerne einen Beleg.
- Meine Vermutung ist, dass RAII nichts anderes ist als "Ressourcen eines Objektes sind an die Lebenszeit des Objektes geknüpft". Wobei "Lebenszeit" einfach die Zeit ist, bis das Objekt aufgeräumt wird - egal wer und wann das Objekt aufgeräumt wird. Somit ist reference-counting oder GC RAII konform. So jedenfalls verstehe ich Stroustrup Seite 354-356 --Sebastian.Dietrich ✉ 00:31, 2. Mai 2015 (CEST)
(Einzug zurückgesetzt)
1) Ja, kann es. Aber die von dir zitierte Aussage aus dem Artikel bezieht sich (meiner Meinung nach) nur auf Objekte mit "automatic storage duration". Dort sorgt RAII dafür, dass gefühlt 90% aller Bugs bezügl. Ressourcen-Lecks etc. zuverlässig verhindert werden.
3) Es kommt eben ganz darauf an, wie der GC arbeitet. Wenn ich daran denke, wie der GC früher in Java definiert war, dann war durchaus legal, dass der GC z.B. nie lief, z.B. weil das Programm zu Ende war, bevor der gesamte an die VM zugewiesene Speicher verbraucht war. Damit ließ sich also kein RAII umsetzen.
Ich denke, dein Vermutung ist im Großen und Ganzen schon richtig. Genaueres kann ich nicht sagen, da ich nicht weiß, wie aktuelle GCs in C++ funktionieren, welche Einschränkungen sie dem Anwender auferlegen und welche Garantien sie dafür bieten. Wahrscheinlich unterscheidet sich das auch noch von Implementierung zu Implementierung. --RokerHRO (Diskussion) 11:21, 2. Mai 2015 (CEST)
- @1) dann ändere ich mal den Satz zu "... und die Freigabe der Betriebsmittel an dessen Destruktoraufruf gebunden. Die automatische Freigabe wird beispielsweise durch das Verlassen des Gültigkeitsbereichs ausgelöst (am Blockende, bei Ausnahmeauslösung, durch Rückgabe an den Aufrufer, usw.), der implizite Destruktoraufruf der Variablen sorgt dann für die Wiederfreigabe der Ressource."
- Damit vertehe ich auch, dass 90% der Ressourcenlecks verhindert werden - aber eben nicht 100% weil eben nicht alles mit "automatic storage duration" abgehandelt werden kann. - darum das "beispielsweise".
- 2) und 3) ich denke man kann gemäß Stroustrup immer RAII-konform programmieren: Alle Ressourcen werden im Konstruktor belegt und (spätestens) im Destruktor freigegeben. In Java wäre das die finalizer Methode, die beim GC läuft, in C++ der Destructor, der beim delete läuft. mMn (und auch lt. Java Empfehlung) ist das zu spät, denn Destruktor oder Finalizer läuft u.U. zu spät. Beispiel: 2 Klassen, die beide gerne abwechselnd auf ein File schreiben bzw. lesen wollen (im gleichen oder auch in verschiedenen Threads). Nachdem Klasse 1 erst die File-Ressource im destructor freigibt, kommt es zu einem Deadlock, der mit try-catch-finally bzw. try-with-ressources nicht passiert wäre.
- @GCs Kein GC, aber auch kein RAII kann garantieren, dass Destruktoren bzw. Finalizer-Methoden aufgerufen werden, da sie nicht garantieren können, dass Objekte korrekt gelöscht werden. Beim nicht-vorhergesehenen Ende eines Programmes (damit meine ich nicht Exceptions, sondern Abstürze, Stromausfälle, SIGKILL) werden Destruktoren genauso nicht aufgerufen wie GCs. Ist mMn auch ein Grund, warum Ressourcen nicht erst im Destructor/Finalizer freigegeben werden sollten. --Sebastian.Dietrich ✉ 20:43, 2. Mai 2015 (CEST)
Ein Blick in den englischsprachigen Artikel zum selben Thema klärt die Frage eigentlich mehr als eindeutig: "(acquisition) is done during object creation (specifically initialization), by the constructor, while resource deallocation (release) is done during object destruction (specifically finalization), by the destructor." und außerdem: "RAII ties resources to object lifetime, which may not coincide with entry and exit of a scope." (nicht signierter Beitrag von 185.53.156.3 (Diskussion) 22:03, 26. Jan. 2017 (CET))
Beispiele
BearbeitenMomentan ist der Artikel etwas C++-lastig. Wäre es nicht sinnvoll, auch ein paar kurze (!) Beispiele aufzuführen, wie das in anderen Programmiersprachen aussieht? Also keine vollständigen Programme oder Funktionen, sondern nur kurze Schnipsel, so dass der Leser einen Eindruck von der Syntax und typischen Verwendung erhält.
In Python wäre das Folgende ein typisches Programmfragment, um die einzelnen Zeilen einer Textdatei in eine Liste zu lesen:
with open("Meine_Datei.txt") as myfile:
lines = myfile.readlines()
Etwas vereinfacht ausgedrückt: Die with
-Anweisung initialisiert in diesem Beispiel die Variable myfile mit einem geöffneten Datei-Objekt, führt dann den zugehörigen Block aus (in diesem Fall werden die Zeilen in Form einer Liste in die Variable lines gelesen), und sorgt im Anschluss dafür, dass die Datei wieder geschlossen wird (auch dann, wenn eine Exception auftritt). --Winof (Diskussion) 18:27, 28. Jun. 2022 (CEST)
- Das ist aber nicht RAII. Für RAII müsste die ressource mit dem Konstruktor belegt und dem Destruktor freigegeben werden. Hier ist aber beides nicht der Fall - die Datei wird einfach am Ende des Blocks geschlossen, freigegeben wird die Ressource aber erst durch den GC.
- Dasselbe kann man in Java mit try-with-ressources erreichen - und ist auch nicht RAII:
try (BufferedReader br = new BufferedReader(new FileReader(path))) {
line = br.readLine();
}
- --Sebastian.Dietrich ✉ 09:09, 29. Jun. 2022 (CEST)
- Standard-Python verwendet Reference-Counting. In dem angegebenen Beispiel wird die Resource mit dem Konstruktor des File-Objekts belegt und initialisiert, und sobald das
with
-Statement abgeschlossen wird (und der Scope verlassen wird), geht der Reference-Counter von myfile auf Null, somit wird die Resource freigegeben. Python hat zwar auch einen Garbage-Collector, aber der spielt in diesem Fall keine Rolle. --Winof (Diskussion) 10:49, 29. Jun. 2022 (CEST) - PS: Man muss hier auch differenzieren bzw. spezifizieren, was mit Ressourcen genau gemeint ist. Die Betriebssystem-Ressourcen, die mit einer geöffneten Datei einhergehen (File-handle bzw. -descriptor, OS-Buffer u. ä.), werden natürlich bereits mit dem Schließen der Datei (
close()
) wieder freigegeben. Die Resourcen, die mit dem Python-Objekt einhergehen, das in diesem Fall der Variablen myfile zugeordnet wird (d. h. ein Eintrag imlocals
-Dictionary mit ein paar Bytes Speicher) wird durch den erwähnten Reference-Counting-Mechanismus entsorgt, sobald der Counter auf Null geht – dies geschieht i. allg. durch Verlassen des Scopes, oder auch durch eine Neuzuweisung an die Variable, oder explizit durch dasdel
-Statement. Wie gesagt; die Freigabe passiert dann unmittelbar; der Garbage-Collector ist nicht involviert. -- Winof (Diskussion) 11:53, 29. Jun. 2022 (CEST)
- Standard-Python verwendet Reference-Counting. In dem angegebenen Beispiel wird die Resource mit dem Konstruktor des File-Objekts belegt und initialisiert, und sobald das
- Naja, per default ist GC in Python aufgedreht. Reference-counting ist ja auch eine Art von garbage-collection. RAII wäre es, wenn bei __del__() die Ressource (also das Python-Objekt) freigegeben werden würde.
- Wenn wir "definieren" (was wir ja in der WP nicht tun), dass hier mit den Ressourcen die Betriebssystem-Ressource der geöffneten Datei gemeint ist, dann gilt für Java dasselbe was auch für Python gilt. Dann könnten de facto viele Programmiersprachen RAII. --Sebastian.Dietrich ✉ 17:11, 29. Jun. 2022 (CEST)
- Das ist nicht ganz korrekt. Beim
del
-Statement wird der Reference-Counter dekrementiert (ebenso wenn eine Variable aus dem Scope herausfällt oder einem anderen Objekt zugewiesen wird). Erreicht der Counter Null – was in dem Beispiel mitwith
der Fall wäre –, dann wird unmittelbar die__del__
-Methode des Objekts aufgerufen und das Objekt freigegeben („destruction“). Das passiert direkt, sofort, garantiert und deterministisch – nicht vielleicht irgendwann später im Hintergrund, wie bei Java. Nochmal: Python hat auch einen Garbage-Collector, aber der ist hier nicht involviert. Der GC in Python dient allein dem Zweck, zyklische Referenzen aufzulösen, die per Reference-Counting nicht erkannt werden. Das ist hier nicht der Fall (und kommt auch sonst in der Praxis nur selten vor; in der Regel kann man den GC ohne schädliche Effekte disablen). Anders ausgedrückt: Python verwendet zwei unterschiedliche Mechanismen für sein Memory-Management, nämlich deterministisches Reference-Counting im Regelfall, und (optional) Garbage-Collection für zyklische Referenzen. --Winof (Diskussion) 17:43, 29. Jun. 2022 (CEST)
- Das ist nicht ganz korrekt. Beim
- Ok, dann wird in dem Beispiel mit
with
sowohl die Ressource myfile als auch der File-Handle freigegeben. Aber nur wenn das Speicher Freigeben auf Grund von Reference-Count=0 unmittelbar erfolgt (dazu habe ich nichts gefunden), also getrefcount(object) niemals 1 (1 wegen der temporären Referenz auf object beim call) liefern kann. - Wenn ich die Dokumentation recht verstehe, dann passiert das File-Handle freigenen (wegen des with-Statements) aber _nicht_ auf Grund der
del
Methode, sondern auf Grund derexit
Methode. D.h. es ist immer noch kein RAII, da eben die "Freigabe der Betriebsmittel <nicht> an dessen Destruktoraufruf gebunden" ist. - Ich weiß das ist akademisch, da ja gleich danach dann der Destruktor kommt, aber hier gehts ja um die Programmiertechnik, die - so wie es der Artikel derzeit darstellt - dadurch erreicht wird, dass Ressourcen beim Destruktor freigegeben werden, was in Python bei entsprechender Programmierung (aber wegen des Reference-Counting und nicht wegen des with-Statements) möglich ist
- D.h. mMn können wir Python jetzt doch in die Liste aufnehmen, aber im Artikel dazuschreiben (was ja auch für alle anderen Programmiersprachen gilt), dass RAII nicht out-of-the-box und überall funktioniert, sondern nur wenn 1) die auf die Ressource verweisende Variable (und somit auch die Ressource) nicht auch außerhalb des Blocks benötigt wird und 2) der Destruktor nicht von einem nebenläufigen(!) Garbage-Collector freigegeben wird (gibts ja auch für C++).
- P.S: Ich habe noch nie eine (nicht triviale) Software ohne zyklische Abhängigkeiten gesehen (was nicht gut ist). D.h. in der Praxis gehe ich davon aus, dass die meisten (nicht trivialen) Programme Memory-Leaks haben, wenn man nur reference-counting verwendet (auch wenn die Programmierer das Gegenteil behaupten). --Sebastian.Dietrich ✉ 06:59, 30. Jun. 2022 (CEST)
- Ok, dann wird in dem Beispiel mit