Warum kann Java / C # RAII nicht implementieren?

29

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?

mike30
quelle
3
Es ist nicht unpraktisch, es ist unmöglich . Der C # Standard ist keine Garantie Destruktoren / Disposes 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.
Telastyn
20
@Telastyn Huh? Was der C # -Standard jetzt sagt, ist nicht relevant, da wir gerade die Änderung dieses Dokuments diskutieren. Die einzige Frage ist, ob dies praktikabel ist, und das einzig Interessante an dem gegenwärtigen Mangel an Garantie sind die Gründe für diesen Mangel an Garantie. Beachten Sie, dass für usingdie Ausführung der Dispose ist garantiert (na ja, den Prozess Diskontierung plötzlich ohne Ausnahme Sterben geworfen, an welcher Stelle alle Bereinigungs vermutlich strittig wird).
4
Duplikat von Haben die Java-Entwickler RAII bewusst aufgegeben? , obwohl die akzeptierte Antwort völlig falsch ist. Die kurze Antwort ist, dass Java eine Referenzsemantik (Heap) anstelle einer Wertsemantik (Stack) verwendet , sodass eine deterministische Finalisierung nicht sehr nützlich / möglich ist. C # verfügt zwar über eine Wertesemantik ( struct), diese wird jedoch in der Regel vermieden, es sei denn, es handelt sich um sehr spezielle Fälle. Siehe auch .
BlueRaja - Danny Pflughoeft
2
Es ist ähnlich, keine exakte Kopie.
Maniero
3
blogs.msdn.com/b/oldnewthing/archive/2010/08/10/10048150.aspx ist eine relevante Seite für diese Frage.
Maniero

Antworten:

17

Eine solche Spracherweiterung wäre erheblich komplizierter und invasiver als Sie denken. Sie können nicht einfach hinzufügen

Wenn die Lebensdauer einer Variablen eines stapelgebundenen Typs endet, rufen Sie Disposedas Objekt auf , auf das sie verweist

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):

File openSavegame(string id) {
    string path = ... id ...;
    File f = new File(path);
    // do something, perhaps logging
    return f;
} // f goes out of scope, caller receives a closed file

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 structs, 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
Warum brauchen Sie einen Konstruktor zum Kopieren / Verschieben? Datei Standbilder ein Referenztyp. In dieser Situation wird f, das ein Zeiger ist, zum Aufrufer kopiert und ist dafür verantwortlich, die Ressource zu entsorgen (der Compiler würde stattdessen implizit ein Try-finally-Dispose-Muster in den Aufrufer
einfügen
1
@bigown Wenn Sie jeden Verweis auf Filediese Weise behandeln, ändert sich nichts und Disposewird nie aufgerufen. Wenn Sie immer anrufen Dispose, 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.
Ich verstehe nicht, was Sie jetzt gesagt haben (ich sage nicht, dass Sie falsch liegen). Das Objekt hat eine Ressource, nicht die Referenz.
Maniero
Ich verstehe, dass der Compiler einen Versuch unmittelbar vor der Ressourcenerfassung (Zeile 3 in Ihrem Beispiel) und den Block finally-dispose kurz vor dem Ende des Gültigkeitsbereichs (Zeile 6) einfügen würde. Kein Problem hier, stimme zu? Zurück zu deinem Beispiel. Der Compiler sieht eine Übergabe, er konnte hier kein try-finally einfügen, aber der Aufrufer erhält ein (Zeiger auf) File-Objekt. Vorausgesetzt, der Aufrufer überträgt dieses Objekt nicht erneut, fügt der Compiler dort ein try-finally-Muster ein. Mit anderen Worten, jedes nicht übertragene IDisposable-Objekt muss ein Try-finally-Muster anwenden.
Maniero
1
@bigown Mit anderen Worten, rufen Sie nicht an, Disposewenn 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, sollte f.Disposeam Ende des Gültigkeitsbereichs aufgerufen werden? Wenn ja, unterbrechen Sie Anrufer, die fzur späteren Verwendung gespeichert werden. Wenn nicht, verlieren Sie die Ressource, wenn der Anrufer nicht speichert f. 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.
26

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:

class IWrapAResource
{
    private readonly Resource resource;
    public IWrapAResource()
    {
        // Where Resource is scope bound
        Resource builder = new Resource(args, args, args);

        this.resource = builder;
    } // Uh oh, resource is destroyed
} // Crap, there's no scope for IWrapAResource we can bind to!

Was schlimmer ist, ist, dass dies für den Implementierer von IWrapAResource:

class IWrapSomething<T>
{
    private readonly T resource; // What happens if T is Resource?
    public IWrapSomething(T input)
    {
        this.resource = input;
    }
}

Etwas wie die usingAussage 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 ist using.

Billy ONeal
quelle
Angenommen, Sie müssen nicht auf eine Variable verweisen, nachdem sie nicht mehr im Geltungsbereich liegt (und es sollte wirklich keine solche Notwendigkeit geben), dann behaupte ich, dass Sie ein Objekt dennoch selbstdispositionieren können, indem Sie einen Finalizer dafür schreiben . Der Finalizer wird aufgerufen, kurz bevor das Objekt überflüssig wird. Siehe msdn.microsoft.com/en-us/library/0s71x931.aspx
Robert Harvey
8
@Robert: Ein korrekt geschriebenes Programm kann nicht davon ausgehen, dass Finalizer jemals ausgeführt werden. blogs.msdn.com/b/oldnewthing/archive/2010/08/09/10047586.aspx
Billy ONeal
1
Hm. Nun, das ist wahrscheinlich der Grund, warum sie diese usingAussage gemacht haben.
Robert Harvey
2
Genau. Dies ist eine riesige Quelle für Newbie-Bugs in C ++ und würde auch in Java / C # vorkommen. Java / C # beseitigt nicht die Möglichkeit, den Verweis auf eine zu zerstörende Ressource zu verlieren. Indem es jedoch explizit und optional angegeben wird, erinnert es den Programmierer daran und gibt ihm die bewusste Wahl, was zu tun ist.
Aleksandr Dubinsky
1
@svick Es liegt nicht an IWrapSomethingzu entsorgen T. Wer auch immer erschaffen hat, Tmuss sich darüber Gedanken machen, ob er es nutzt using, es IDisposableselbst ist oder ein Ad-hoc-Ressourcen-Lebenszyklusschema hat.
Aleksandr Dubinsky
13

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 newVerwendung von Zeigern).

In C ++ können Sie also Folgendes tun:

void f()
{
    Foo f1;
    Foo* f2 = new Foo();
    Foo::someStaticField = f2;

    // f1 is destroyed here, the object pointed to by f2 isn't
}

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:

void f()
{
    Foo f1;
    Foo^ f2 = gcnew Foo();
    Foo::someStaticField = f2;

    // f1 is disposed here, the object pointed to by f2 isn't
}

Dies kompiliert im Prinzip dieselbe IL wie das folgende C #:

void f()
{
    using (Foo f1 = new Foo())
    {
        Foo f2 = new Foo();
        Foo.someStaticField = f2;
    }
    // f1 is disposed here, the object pointed to by f2 isn't
}

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.

svick
quelle
1
C # könnte RAII trivial ausführen, wenn es Destruktoren für structTypen wie D zulässt.
Jan Hudec
6

Wenn Sie an usingBlö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:

public void ReadFile ()
{
  string filename = "myFile.dat";
  local Stream file = File.Open(filename);
  file.Read(blah blah blah);
}

Siehe das localKeyword, das ich hinzugefügt habe? Alles, was es tut, ist ein bisschen mehr syntaktischen Zucker hinzuzufügen, genau wie wenn usingder Compiler angewiesen wird, Disposeeinen finallyBlock am Ende des Gültigkeitsbereichs der Variablen aufzurufen . Das ist alles. Es ist völlig gleichbedeutend mit:

public void ReadFile ()
{
  string filename = "myFile.dat";
  using (Stream file = File.Open(filename))
  {
      file.Read(blah blah blah);
  }
}

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.

Avner Shahar-Kashtan
quelle
1
@ mike30, aber wenn Sie es in die Typdefinition verschieben, gelangen Sie genau zu den anderen aufgeführten Problemen. Was passiert, wenn Sie den Zeiger auf eine andere Methode übergeben oder ihn von der Funktion zurückgeben? Auf diese Weise wird der Geltungsbereich im Geltungsbereich deklariert, nicht anderswo. Ein Typ könnte Disposable sein, aber es ist nicht an ihm, Dispose aufzurufen.
Avner Shahar-Kashtan
3
@ mike30: Meh. Mit dieser Syntax werden lediglich die geschweiften Klammern und damit auch die von ihnen bereitgestellten Steuerungsmöglichkeiten für den Gültigkeitsbereich entfernt.
Robert Harvey
1
Genau. Es opfert etwas Flexibilität für saubereren, weniger verschachtelten Code. Wenn wir @delnans Vorschlag annehmen und das usingSchlü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. usingStandardmäßig ohne geschweifte Klammern für den aktuellen Bereich.
Avner Shahar-Kashtan
1
Ich habe keine Probleme mit semi-praktischen Übungen im Sprachdesign.
Avner Shahar-Kashtan
1
@RobertHarvey. Sie scheinen Vorurteile gegen alles zu haben, was derzeit nicht in C # implementiert ist. Wir hätten keine Generics, Linq, Using-Blocks, Ipmlicit-Typen usw., wenn wir mit C # 1.0 zufrieden wären. Diese Syntax löst nicht das Problem der Implizität, ist aber ein guter Zucker, um sich an den aktuellen Geltungsbereich zu binden.
mike30
1

Überprüfen Sie das withSchlü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:

with open('output.txt', 'w') as f:
    f.write('Hi there!')

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 " breakSofort -Exit" returnoder 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:

def __enter__(self):
  return self
def __exit__(self):
  self.close()

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.

Javier
quelle
7
within Python ist fast genau wie usingin C #, und als solches nicht RAII, soweit diese Frage betroffen ist.
1
Pythons "mit" ist bereichsgebundene Ressourcenverwaltung, aber es fehlt die Implizite eines intelligenten Zeigers. Das Deklarieren eines Zeigers als "intelligent" kann als "explizit" betrachtet werden. Wenn der Compiler jedoch die Intelligenz als Teil des Objekttyps erzwingt, tendiert er zu "implizit".
mike30
AFAICT, das Ziel von RAII ist es, einen strikten Bereich für Ressourcen festzulegen. Wenn Sie nur daran interessiert sind, Objekte freizugeben, können es die Sprachen, die mit Müll gesammelt wurden, nicht. Wenn Sie daran interessiert sind, Ressourcen konsequent freizugeben, ist dies eine Möglichkeit (eine andere ist deferin der Sprache Go).
Javier
1
Eigentlich halte ich es für fair zu sagen, dass Java und C # explizite Konstruktionen stark bevorzugen. Warum sollte man sich sonst mit der ganzen Zeremonie befassen, die mit der Verwendung von Schnittstellen und Vererbung verbunden ist?
Robert Harvey
1
@delnan, Go hat 'implizite' Schnittstellen.
Javier