Frage: Warum kann Java / C # RAII nicht implementieren?
Klarstellung: Mir ist bewusst, dass der Müllsammler nicht deterministisch ist. Mit den aktuellen Sprachfunktionen ist es daher nicht möglich, dass die Dispose () - Methode eines Objekts beim Verlassen des Gültigkeitsbereichs automatisch aufgerufen wird. Aber könnte ein solches deterministisches Merkmal hinzugefügt werden?
Mein Verständnis:
Ich bin der Meinung, dass eine Implementierung von RAII zwei Anforderungen erfüllen muss:
1. Die Lebensdauer einer Ressource muss an einen Bereich gebunden sein.
2. Implizit. Die Freigabe der Ressource muss ohne ausdrückliche Anweisung des Programmierers erfolgen. Analog zu einem Garbage Collector, der Speicher ohne explizite Anweisung freigibt. Die "implizite Aussage" muss nur am Verwendungsort der Klasse erfolgen. Der Klassenbibliothek-Ersteller muss natürlich explizit einen Destruktor oder eine Dispose () -Methode implementieren.
Java / C # -erfüllungspunkt 1. In C # kann eine Ressource, die IDisposable implementiert, an einen Gültigkeitsbereich "using" gebunden werden:
void test()
{
using(Resource r = new Resource())
{
r.foo();
}//resource released on scope exit
}
Dies erfüllt Punkt 2 nicht. Der Programmierer muss das Objekt explizit an einen speziellen Verwendungsbereich binden. Programmierer können (und müssen) vergessen, die Ressource explizit an einen Bereich zu binden, wodurch ein Leck entsteht.
Tatsächlich werden die using-Blöcke vom Compiler in try-finally-dispose () - Code konvertiert. Es hat die gleiche explizite Natur des try-finally-dispose () - Musters. Ohne eine implizite Freigabe ist der Haken zu einem Bereich syntaktischer Zucker.
void test()
{
//Programmer forgot (or was not aware of the need) to explicitly
//bind Resource to a scope.
Resource r = new Resource();
r.foo();
}//resource leaked!!!
Ich denke, es lohnt sich, ein Sprachfeature in Java / C # zu erstellen, das spezielle Objekte ermöglicht, die über einen Smart-Pointer mit dem Stack verknüpft sind. Mit dieser Funktion können Sie eine Klasse als bereichsgebunden kennzeichnen, sodass sie immer mit einem Haken zum Stapel erstellt wird. Es könnte Optionen für verschiedene Arten von intelligenten Zeigern geben.
class Resource - ScopeBound
{
/* class details */
void Dispose()
{
//free resource
}
}
void test()
{
//class Resource was flagged as ScopeBound so the tie to the stack is implicit.
Resource r = new Resource(); //r is a smart-pointer
r.foo();
}//resource released on scope exit.
Ich denke, implizite Aussagen sind es "wert". Genau so, wie es sich "lohnt", Müll zu sammeln. Explizite Verwendung von Blöcken erfrischt die Augen, bietet jedoch keinen semantischen Vorteil gegenüber try-finally-dispose ().
Ist es unpraktisch, eine solche Funktion in Java / C # -Sprachen zu implementieren? Könnte es eingeführt werden, ohne alten Code zu brechen?
Dispose
s wird immer ausgeführt, unabhängig davon , wie sie ausgelöst sind. Das Hinzufügen einer impliziten Zerstörung am Ende des Geltungsbereichs hilft dem nicht.using
die Ausführung derDispose
ist garantiert (na ja, den Prozess Diskontierung plötzlich ohne Ausnahme Sterben geworfen, an welcher Stelle alle Bereinigungs vermutlich strittig wird).struct
), diese wird jedoch in der Regel vermieden, es sei denn, es handelt sich um sehr spezielle Fälle. Siehe auch .Antworten:
Eine solche Spracherweiterung wäre erheblich komplizierter und invasiver als Sie denken. Sie können nicht einfach hinzufügen
zu dem entsprechenden Abschnitt der Sprachspezifikation und getan werden. Ich werde das Problem der temporären Werte (
new Resource().doSomething()
) ignorieren , das durch eine etwas allgemeinere Formulierung gelöst werden kann. Dies ist nicht das schwerwiegendste Problem. Zum Beispiel würde dieser Code kaputt sein (und so etwas wird wahrscheinlich im Allgemeinen unmöglich):Jetzt benötigen Sie benutzerdefinierte Kopierkonstruktoren (oder verschieben Konstruktoren) und können diese überall aufrufen. Dies hat nicht nur Auswirkungen auf die Leistung, sondern führt auch dazu, dass Typen effektiv bewertet werden, während fast alle anderen Objekte Referenztypen sind. In Javas Fall ist dies eine radikale Abweichung von der Funktionsweise von Objekten. In C # weniger (hat bereits
struct
s, aber keine benutzerdefinierten Kopierkonstruktoren für sie AFAIK), aber es macht diese RAII-Objekte noch spezieller. Alternativ kann eine eingeschränkte Version von linearen Typen (vgl. Rust) das Problem auch auf Kosten des Verbots von Aliasing einschließlich Parameterübergabe lösen (es sei denn, Sie möchten durch die Verwendung von Rust-ähnlichen geliehenen Referenzen und einem Leihprüfer noch mehr Komplexität einführen).Es kann technisch gemacht werden, aber Sie landen in einer Kategorie von Dingen, die sich von allem anderen in der Sprache sehr unterscheiden. Dies ist fast immer eine schlechte Idee, mit Konsequenzen für Implementierer (mehr Randfälle, mehr Zeit / Kosten in jeder Abteilung) und Benutzer (mehr Konzepte zum Lernen, mehr Möglichkeit von Fehlern). Es ist den zusätzlichen Komfort nicht wert.
quelle
File
diese Weise behandeln, ändert sich nichts undDispose
wird nie aufgerufen. Wenn Sie immer anrufenDispose
, können Sie mit Einwegobjekten nichts anfangen. Oder schlagen Sie ein Schema vor, um manchmal zu veräußern und manchmal nicht? Wenn ja, beschreiben Sie es bitte ausführlich und ich erkläre Ihnen Situationen, in denen es fehlschlägt.Dispose
wenn eine Referenz entweicht? Die Fluchtanalyse ist ein altes und schweres Problem, das nicht immer funktioniert, wenn die Sprache nicht weiter geändert wird. Wenn eine Referenz an eine andere (virtuelle) Methode (something.EatFile(f);
) übergeben wird, solltef.Dispose
am Ende des Gültigkeitsbereichs aufgerufen werden? Wenn ja, unterbrechen Sie Anrufer, dief
zur späteren Verwendung gespeichert werden. Wenn nicht, verlieren Sie die Ressource, wenn der Anrufer nicht speichertf
. Die einzige etwas einfache Möglichkeit, dies zu beseitigen, ist ein lineares Typensystem, das (wie ich später in meiner Antwort bereits erörtert habe) stattdessen viele andere Komplikationen mit sich bringt.Die größte Schwierigkeit bei der Implementierung dieser Art für Java oder C # wäre die Definition der Funktionsweise der Ressourcenübertragung. Sie benötigen eine Möglichkeit, die Lebensdauer der Ressource über den Rahmen hinaus zu verlängern. Erwägen:
Was schlimmer ist, ist, dass dies für den Implementierer von
IWrapAResource
:Etwas wie die
using
Aussage von C # ist wahrscheinlich so ähnlich wie die von RAII, ohne auf das Zählen von Ressourcen oder das Erzwingen von Wertsemantik überall wie in C oder C ++ zurückzugreifen. Da Java und C # implizit Ressourcen gemeinsam nutzen, die von einem Garbage Collector verwaltet werden, muss ein Programmierer mindestens den Bereich auswählen, an den eine Ressource gebunden istusing
.quelle
using
Aussage gemacht haben.IWrapSomething
zu entsorgenT
. Wer auch immer erschaffen hat,T
muss sich darüber Gedanken machen, ob er es nutztusing
, esIDisposable
selbst ist oder ein Ad-hoc-Ressourcen-Lebenszyklusschema hat.Der Grund, warum RAII in einer Sprache wie C # nicht funktionieren kann, aber in C ++ funktioniert, ist, dass Sie in C ++ entscheiden können, ob ein Objekt wirklich temporär ist (indem Sie es auf dem Stapel zuweisen) oder ob es langlebig ist (von Zuweisung auf dem Heap unter
new
Verwendung von Zeigern).In C ++ können Sie also Folgendes tun:
In C # können Sie nicht zwischen den beiden Fällen unterscheiden, sodass der Compiler keine Ahnung hätte, ob er das Objekt finalisieren soll oder nicht.
Was Sie tun können, ist die Einführung einer speziellen lokalen Variablenart, die Sie nicht in Felder usw. einfügen können. Welches ist genau das, was C ++ / CLI macht. In C ++ / CLI schreiben Sie Code wie folgt:
Dies kompiliert im Prinzip dieselbe IL wie das folgende C #:
Abschließend möchte ich raten, warum die Designer von C # RAII nicht hinzugefügt haben, weil sie der Meinung waren, dass es sich nicht lohnt, zwei verschiedene Arten von lokalen Variablen zu verwenden, vor allem, weil in einer Sprache mit GC eine deterministische Finalisierung nicht sinnvoll ist häufig.
* Nicht ohne das Äquivalent des
&
Operators, der in C ++ / CLI ist%
. Obwohl dies in dem Sinne "unsicher" ist, dass das Feld nach Beendigung der Methode auf ein entsorgtes Objekt verweist.quelle
struct
Typen wie D zulässt.Wenn Sie an
using
Blöcken gestört werden, können wir vielleicht einen kleinen Schritt in Richtung weniger explizite Aussagen machen, anstatt die C # -Spezifikation selbst zu ändern. Betrachten Sie diesen Code:Siehe das
local
Keyword, das ich hinzugefügt habe? Alles, was es tut, ist ein bisschen mehr syntaktischen Zucker hinzuzufügen, genau wie wennusing
der Compiler angewiesen wird,Dispose
einenfinally
Block am Ende des Gültigkeitsbereichs der Variablen aufzurufen . Das ist alles. Es ist völlig gleichbedeutend mit:aber mit einem impliziten Bereich, anstatt einem expliziten. Es ist einfacher als die anderen Vorschläge, da ich die Klasse nicht als bereichsgebunden definieren muss. Nur sauberer, impliziter syntaktischer Zucker.
Möglicherweise gibt es hier Probleme mit schwer zu lösenden Bereichen, obwohl ich es derzeit nicht sehe, und ich würde es jedem danken, der es finden kann.
quelle
using
Schlüsselwort wiederverwenden , können wir das vorhandene Verhalten beibehalten und dieses auch für die Fälle verwenden, in denen wir den spezifischen Geltungsbereich nicht benötigen.using
Standardmäßig ohne geschweifte Klammern für den aktuellen Bereich.Überprüfen Sie das
with
Schlüsselwort in Python , um ein Beispiel für die Funktionsweise von RAII in einer Garbage-Collected-Sprache zu erhalten . Statt auf determinis zerstörten Objekte zu verlassen, ist es können Sie assoziieren__enter__()
und__exit__()
Methoden zu einem lexikalischen Umfang gegeben. Ein häufiges Beispiel ist:Wie beim RAII-Stil von C ++ wird die Datei beim Verlassen dieses Blocks geschlossen, unabhängig davon, ob es sich um einen "normalen" Exit, einen "
break
Sofort -Exit"return
oder einen "Ausnahmebedingungs -Exit" handelt .Beachten Sie, dass der
open()
Aufruf die übliche Funktion zum Öffnen von Dateien ist. Damit dies funktioniert, enthält das zurückgegebene Dateiobjekt zwei Methoden:Dies ist eine gebräuchliche Redewendung in Python: Objekte, die einer Ressource zugeordnet sind, enthalten normalerweise diese beiden Methoden.
Beachten Sie, dass das Dateiobjekt auch nach dem
__exit__()
Aufruf zugewiesen bleiben kann. Wichtig ist, dass es geschlossen ist.quelle
with
in Python ist fast genau wieusing
in C #, und als solches nicht RAII, soweit diese Frage betroffen ist.defer
in der Sprache Go).