Haben die Java-Entwickler RAII bewusst aufgegeben?

82

Als langjähriger C # -Programmierer habe ich kürzlich mehr über die Vorteile von RAII ( Resource Acquisition Is Initialization ) erfahren . Insbesondere habe ich festgestellt, dass die C # -Sprache:

using (var dbConn = new DbConnection(connStr)) {
    // do stuff with dbConn
}

hat das C ++ Äquivalent:

{
    DbConnection dbConn(connStr);
    // do stuff with dbConn
}

Das bedeutet, dass es in C ++ unnötig ist, die Verwendung von Ressourcen wie DbConnectionin einem usingBlock einzuschließen ! Dies scheint ein großer Vorteil von C ++ zu sein. Dies ist umso überzeugender, wenn Sie beispielsweise eine Klasse betrachten, die ein Instanzmitglied vom Typ DbConnectionhat

class Foo {
    DbConnection dbConn;

    // ...
}

In C # müsste ich Foo IDisposableals solches implementieren :

class Foo : IDisposable {
    DbConnection dbConn;

    public void Dispose()
    {       
        dbConn.Dispose();
    }
}

und was noch schlimmer ist, jeder Benutzer von Foomüsste daran denken, Fooin einem usingBlock Folgendes einzuschließen :

   using (var foo = new Foo()) {
       // do stuff with "foo"
   }

Wenn ich mir jetzt C # und seine Java-Wurzeln anschaue, frage ich mich, ob die Java-Entwickler wirklich verstanden haben, was sie aufgegeben haben, als sie den Stack zugunsten des Heaps aufgegeben haben und damit RAII aufgegeben haben.

(Hat Stroustrup auch die Bedeutung von RAII voll und ganz erkannt?)

JoelFan
quelle
5
Ich bin nicht sicher, wovon Sie sprechen, wenn Sie keine Ressourcen in C ++ einschließen. Das DBConnection-Objekt behandelt wahrscheinlich das Schließen aller Ressourcen in seinem Destruktor.
maple_shaft
16
@maple_shaft, genau mein Punkt! Das ist der Vorteil von C ++, den ich in dieser Frage anspreche. In C # müssen Sie Ressourcen in "using" einschließen ... in C ++ tun Sie dies nicht.
JoelFan
12
Ich verstehe, dass RAII als Strategie nur verstanden wurde, wenn C ++ - Compiler gut genug waren, um fortgeschrittenes Templating zu verwenden, das weit hinter Java zurückliegt. Das C ++, das zum Zeitpunkt der Erstellung von Java tatsächlich zur Verfügung stand, war ein sehr primitives C mit Klassen-Stil, mit möglicherweise grundlegenden Vorlagen, wenn Sie Glück hatten.
Sean McMillan
6
"Ich verstehe, dass RAII als Strategie nur verstanden wurde, wenn C ++ - Compiler gut genug waren, um fortgeschrittenes Templating zu verwenden, das Java weit nachsteht." - Das stimmt nicht wirklich. Konstruktoren und Destruktoren sind seit dem ersten Tag Kernfunktionen von C ++, lange vor der weit verbreiteten Verwendung von Vorlagen und lange vor Java.
Jim In Texas
8
@JimInTexas: Ich denke, Sean hat irgendwo einen grundlegenden Keim der Wahrheit (obwohl nicht Vorlagen, sondern Ausnahmen die Krux sind). Konstrukteure / Destruktoren waren von Anfang an dabei, aber die Bedeutung und das Konzept von RAII wurden anfangs (nach was für einem Wort ich suche) nicht realisiert. Es hat ein paar Jahre und einige Zeit gedauert, bis die Compiler gut wurden, bis wir begriffen haben, wie wichtig die gesamte RAII ist.
Martin York

Antworten:

38

Wenn ich mir jetzt C # und seine Java-Wurzeln anschaue, frage ich mich, ob die Java-Entwickler wirklich verstanden haben, was sie aufgegeben haben, als sie den Stack zugunsten des Heaps aufgegeben haben und damit RAII aufgegeben haben.

(Hat Stroustrup auch die Bedeutung von RAII voll und ganz erkannt?)

Ich bin mir ziemlich sicher, dass Gosling zu der Zeit, als er Java entwarf, die Bedeutung von RAII nicht verstanden hat. In seinen Interviews sprach er oft über Gründe, um Generika und das Überladen von Operatoren auszulassen, erwähnte aber niemals deterministische Destruktoren und RAII.

Komischerweise war sich selbst Stroustrup nicht bewusst, wie wichtig deterministische Destruktoren waren, als er sie entwarf. Ich kann das Zitat nicht finden, aber wenn Sie wirklich daran interessiert sind, können Sie es in seinen Interviews hier finden: http://www.stroustrup.com/interviews.html

Nemanja Trifunovic
quelle
11
@maple_shaft: Kurz gesagt, es ist nicht möglich. Wenn Sie nicht einen Weg gefunden hätten, eine deterministische Garbage Collection zu haben (was im Allgemeinen unmöglich erscheint und auf jeden Fall alle GC-Optimierungen der letzten Jahrzehnte ungültig macht), müssten Sie stapelzugeordnete Objekte einführen, aber das öffnet mehrere Dosen mit Würmer: Diese Objekte benötigen eine Semantik, ein "Slicing-Problem" bei der Untertypisierung (und somit KEIN Polymorphismus) und baumelnde Zeiger, es sei denn, Sie legen möglicherweise erhebliche Einschränkungen fest oder nehmen massive inkompatible Änderungen am Typsystem vor. Und das ist einfach unglaublich.
13
@DeadMG: Sie schlagen also vor, dass wir zur manuellen Speicherverwaltung zurückkehren. Das ist eine allgemein gültige Herangehensweise an die Programmierung und erlaubt natürlich eine deterministische Zerstörung. Das beantwortet jedoch nicht diese Frage, die sich mit einer reinen GC-Einstellung befasst, die Speichersicherheit und genau definiertes Verhalten bieten möchte, auch wenn wir uns alle wie Idioten verhalten. Das erfordert GC für alles und keine Möglichkeit, die Objektzerstörung manuell auszulösen (und der gesamte vorhandene Java-Code beruht zumindest auf dem ersteren). Entweder machen Sie GC deterministisch oder Sie haben Pech.
26
@delan. Ich würde C ++ Smart Pointer manualMemory Management nicht nennen . Sie sind eher wie ein deterministischer, feinkörniger, steuerbarer Müllsammler. Bei richtiger Anwendung sind intelligente Zeiger die Bienenknie.
Martin York
10
@LokiAstari: Nun, ich würde sagen, sie sind etwas weniger automatisch als die vollständige GC (Sie müssen darüber nachdenken, welche Art von Intelligenz Sie tatsächlich wollen) und die Implementierung als Bibliothek erfordert rohe Zeiger (und damit manuelle Speicherverwaltung), um darauf aufzubauen . Außerdem ist mir kein intelligenter Zeiger bekannt, der zyklische Verweise automatisch verarbeitet. Dies ist eine strenge Voraussetzung für die Garbage Collection in meinen Büchern. Intelligente Zeiger sind mit Sicherheit unglaublich cool und nützlich, aber Sie müssen sich damit abfinden, dass sie keine Garantie für eine vollständige und ausschließliche GC-Sprache bieten (unabhängig davon, ob Sie sie für nützlich halten oder nicht).
11
@delan: da muss ich nicht zustimmen. Ich denke, sie sind automatischer als GC, da sie deterministisch sind. OKAY. Um effizient zu sein, müssen Sie sicherstellen, dass Sie die richtige verwenden (das gebe ich Ihnen). std :: weak_ptr verarbeitet Zyklen perfekt. Zyklen werden immer ausgelassen, aber in Wirklichkeit ist es kaum ein Problem, da das Basisobjekt normalerweise stapelbasiert ist und wenn dies geschieht, wird der Rest aufgeräumt. In den seltenen Fällen kann es ein Problem sein std :: weak_ptr.
Martin York
60

Ja, die Designer von C # (und, da bin ich mir sicher, Java) haben sich ausdrücklich gegen eine deterministische Finalisierung entschieden. Ich habe Anders Hejlsberg etwa 1999-2002 mehrmals danach gefragt.

Erstens widerspricht die Vorstellung einer unterschiedlichen Semantik für ein Objekt, die darauf basiert, ob es stapel- oder haufenbasiert ist, zweifellos dem vereinheitlichenden Entwurfsziel beider Sprachen, das darin bestand, Programmierer von genau solchen Problemen zu befreien.

Zweitens gibt es, selbst wenn Sie anerkennen, dass es Vorteile gibt, erhebliche Implementierungskomplexitäten und Ineffizienzen bei der Buchführung. Sie können keine stapelähnlichen Objekte in einer verwalteten Sprache auf den Stapel legen. Es bleibt Ihnen nichts anderes übrig, als "stapelartige Semantik" zu sagen und sich einer wichtigen Arbeit zu widmen (Werttypen sind bereits hart genug, denken Sie an ein Objekt, das eine Instanz einer komplexen Klasse ist, mit eingehenden und in den verwalteten Speicher zurückgehenden Referenzen).

Aus diesem Grund möchten Sie nicht für jedes Objekt in einem Programmiersystem eine deterministische Finalisierung, bei der "(fast) alles ein Objekt ist". Sie müssen also eine Art programmierergesteuerte Syntax einführen, um ein normal verfolgtes Objekt von einem Objekt mit deterministischer Finalisierung zu trennen.

In C # haben Sie das usingSchlüsselwort, das ziemlich spät im Entwurf von C # 1.0 eingegangen ist. Das IDisposableGanze ist ziemlich elend, und man fragt sich, ob es eleganter wäre, usingmit der C ++ - Destruktorsyntax ~diejenigen Klassen zu kennzeichnen, auf die das Boiler-Plate- IDisposableMuster automatisch angewendet werden könnte.

Larry OBrien
quelle
2
Was ist mit dem, was C ++ / CLI (.NET) getan hat, wobei die Objekte auf dem verwalteten Heap auch ein stapelbasiertes "Handle" haben, das RIAA bereitstellt?
JoelFan
3
C ++ / CLI hat ganz andere Entwurfsentscheidungen und -einschränkungen. Einige dieser Entscheidungen bedeuten, dass Sie den Programmierern mehr Gedanken über die Speicherzuweisung und die Auswirkungen auf die Leistung abverlangen können: Der gesamte Kompromiss "Gib ihnen genug Seil, um sich aufzuhängen". Und ich stelle mir vor, dass der C ++ / CLI-Compiler erheblich komplexer ist als der von C # (insbesondere in den frühen Generationen).
Larry OBrien
5
+1 Dies ist die einzig richtige Antwort, da Java absichtlich keine (nicht primitiven) stapelbasierten Objekte hat.
BlueRaja - Danny Pflughoeft
8
@ Peter Taylor - richtig. Ich bin jedoch der Meinung, dass der nicht deterministische Destruktor von C # sehr wenig wert ist, da Sie sich nicht darauf verlassen können, dass er eine eingeschränkte Ressource verwaltet. Meiner Meinung nach wäre es daher besser gewesen, die ~Syntax als syntaktischen Zucker fürIDisposable.Dispose()
Larry OBrien,
3
@ Larry: Ich stimme zu. C ++ / CLI nicht verwenden ~als syntaktischer Zucker für IDisposable.Dispose(), und es ist viel bequemer als die C # Syntax.
Dan04
41

Denken Sie daran, dass Java 1991-1995 entwickelt wurde, als C ++ eine ganz andere Sprache war. Ausnahmen (die RAII erforderlich machten ) und Vorlagen (die das Implementieren von intelligenten Zeigern erleichterten) waren neue Funktionen. Die meisten C ++ - Programmierer stammten aus C und waren an die manuelle Speicherverwaltung gewöhnt.

Daher bezweifle ich, dass Javas Entwickler sich bewusst entschieden haben, RAII aufzugeben. Es war jedoch eine bewusste Entscheidung für Java, Referenzsemantik statt Wertsemantik zu bevorzugen. Deterministische Zerstörung ist in einer referenzsemantischen Sprache schwer zu implementieren.

Warum also Referenzsemantik anstelle von Wertsemantik verwenden?

Weil es die Sprache viel einfacher macht.

  • Eine syntaktische Unterscheidung zwischen Foound Foo*oder zwischen foo.barund ist nicht erforderlich foo->bar.
  • Es ist keine überladene Zuweisung erforderlich, wenn alle Zuweisungen das Kopieren eines Zeigers erfordern.
  • Es werden keine Kopierkonstruktoren benötigt. (Es gibt gelegentlich einen Bedarf für eine explizite Kopierfunktion wie clone(). Viele Objekte müssen nur nicht kopiert werden. Zum Beispiel immutables dies nicht tun.)
  • Es ist nicht erforderlich, privateKopierkonstruktoren zu deklarieren und operator=eine Klasse nicht kopierbar zu machen. Wenn Sie nicht möchten, dass Objekte einer Klasse kopiert werden, schreiben Sie einfach keine Funktion, um sie zu kopieren.
  • Es werden keine swapFunktionen benötigt. (Es sei denn, Sie schreiben eine Sortierroutine.)
  • Es werden keine C ++ - R-Wert-Referenzen im 0x-Stil benötigt.
  • (N) RVO ist nicht erforderlich.
  • Es gibt kein Schneideproblem.
  • Für den Compiler ist es einfacher, Objektlayouts zu bestimmen, da Referenzen eine feste Größe haben.

Der Hauptnachteil der Referenzsemantik besteht darin, dass es schwierig wird, zu wissen, wann ein Objekt gelöscht werden muss, wenn möglicherweise mehrere Referenzen vorhanden sind. Sie müssen so ziemlich über eine automatische Speicherverwaltung verfügen.

Java hat sich für einen nicht deterministischen Garbage Collector entschieden.

Kann GC nicht deterministisch sein?

Ja, kann es. Beispielsweise verwendet die C-Implementierung von Python die Referenzzählung. Und später Tracing-GC hinzugefügt, um den zyklischen Müll zu handhaben, bei dem die Nachzählung fehlschlägt.

Aber das Nachzählen ist schrecklich ineffizient. Viele CPU-Zyklen haben die Anzahl aktualisiert. Schlimmer noch in einer Umgebung mit mehreren Threads (für die Java entwickelt wurde), in der diese Updates synchronisiert werden müssen. Es ist viel besser, den Null-Garbage-Collector zu verwenden, bis Sie zu einem anderen wechseln müssen.

Man könnte sagen, dass Java sich entschieden hat, den allgemeinen Fall (Speicher) auf Kosten nicht fungibler Ressourcen wie Dateien und Sockets zu optimieren. Angesichts der Einführung von RAII in C ++ scheint dies heute die falsche Wahl zu sein. Aber denken Sie daran, dass ein Großteil der Zielgruppe für Java C-Programmierer (oder "C mit Klassen") waren, die es gewohnt waren, diese Dinge explizit zu schließen.

Aber was ist mit C ++ / CLI "Stack-Objekten"?

Sie sind nur syntaktischer Zucker fürDispose ( Original-Link ), ähnlich wie C # using. Es löst jedoch nicht das allgemeine Problem der deterministischen Zerstörung, da Sie eine anonyme erstellen können gcnew FileStream("filename.ext")und C ++ / CLI diese nicht automatisch entsorgt.

dan04
quelle
3
Auch nette Links (vor allem der erste, der für diese Diskussion sehr relevant ist) .
BlueRaja - Danny Pflughoeft
Die usingAnweisung behandelt viele Probleme im Zusammenhang mit der Bereinigung gut, viele andere bleiben jedoch bestehen. Ich würde vorschlagen, dass der richtige Ansatz für eine Sprache und ein Framework darin besteht, deklarativ zwischen Speicherorten zu unterscheiden, die einen Verweis "besitzen", und IDisposablesolchen, die dies nicht tun. Das Überschreiben oder Verlassen eines Speicherorts, der einen referenzierten Speicherort besitzt, IDisposablesollte das Ziel in Ermangelung einer gegenteiligen Richtlinie veranlassen.
Supercat
1
"Keine Notwendigkeit für Kopierkonstruktoren" hört sich nett an, scheitert aber in der Praxis. java.util.Date und Calendar sind vielleicht die bekanntesten Beispiele. Nichts schöner als new Date(oldDate.getTime()).
Kevin Cline
2
iow RAII war nicht "aufgegeben", es existierte einfach nicht, um aufgegeben zu werden :) Was Konstruktoren angeht, ich habe sie nie gemocht (else) hat vergessen, eine tiefe Kopie zu erstellen, sodass Ressourcen zwischen Kopien geteilt werden, die nicht vorhanden sein sollten.
16.
20

Java7 hat etwas Ähnliches wie C # eingeführt using: Die Anweisung try-with-resources

eine tryAnweisung, die eine oder mehrere Ressourcen deklariert. Eine Ressource ist ein Objekt, das geschlossen werden muss, nachdem das Programm damit fertig ist. Die tryAnweisung -with-resources stellt sicher, dass jede Ressource am Ende der Anweisung geschlossen wird. Jedes Objekt, das implementiert wird java.lang.AutoCloseable, einschließlich aller Objekte, die implementiert werden java.io.Closeable, kann als Ressource verwendet werden ...

Ich denke, sie haben sich entweder nicht bewusst dafür entschieden, RAII nicht zu implementieren, oder sie haben es sich inzwischen anders überlegt.

Patrick
quelle
Interessant, aber es sieht so aus, als ob dies nur mit Objekten funktioniert, die implementieren java.lang.AutoCloseable. Wahrscheinlich keine große Sache, aber ich mag es nicht, wie sich das etwas eingeschränkt anfühlt. Vielleicht habe ich ein anderes Objekt, das automatisch freigegeben werden sollte, aber es ist sehr seltsam, es implementieren zu lassen AutoCloseable...
FrustratedWithFormsDesigner
9
@Patrick: Äh, also? usingist nicht dasselbe wie RAII - in einem Fall kümmert sich der Anrufer um die Verteilung der Ressourcen, in dem anderen Fall kümmert sich der Angerufene darum.
BlueRaja - Danny Pflughoeft
1
+1 Ich wusste nichts über das Ausprobieren von Ressourcen. Es sollte nützlich sein, um mehr Dampfkessel zu entleeren.
jprete
3
-1 für using/ try-with-resources stimmt nicht mit RAII überein.
Sean McMillan
4
@ Sean: Einverstanden. usingund es ist ilk sind bei weitem nicht RAII.
DeadMG
18

Java hat absichtlich keine stapelbasierten Objekte (auch Wertobjekte genannt). Diese sind notwendig, damit das Objekt am Ende der Methode automatisch zerstört wird.

Aus diesem Grund und aufgrund der Tatsache, dass Java durch Garbage-Collection erfasst wird, ist eine deterministische Finalisierung nahezu unmöglich (z. B. Was passiert, wenn mein "lokales" Objekt an einer anderen Stelle referenziert wird? Wenn die Methode endet, möchten wir, dass es nicht zerstört wird ) .

Dies ist jedoch für die meisten von uns in Ordnung, da fast nie eine deterministische Finalisierung erforderlich ist , außer bei der Interaktion mit nativen (C ++) Ressourcen!


Warum hat Java keine stapelbasierten Objekte?

(Andere als Primitive ..)

Weil stapelbasierte Objekte eine andere Semantik haben als heapbasierte Referenzen. Stellen Sie sich den folgenden Code in C ++ vor; was tut es?

return myObject;
  • Wenn myObjectes sich um ein lokales stapelbasiertes Objekt handelt, wird der Kopierkonstruktor aufgerufen (wenn das Ergebnis einer bestimmten Funktion zugewiesen ist).
  • Wenn myObjectes sich um ein lokales stapelbasiertes Objekt handelt und wir eine Referenz zurückgeben, ist das Ergebnis undefiniert.
  • Wenn myObjectes sich um ein Element / globales Objekt handelt, wird der Kopierkonstruktor aufgerufen (wenn das Ergebnis einer bestimmten Funktion zugewiesen ist).
  • Wenn myObjectes sich um ein Element / globales Objekt handelt und wir eine Referenz zurückgeben, wird die Referenz zurückgegeben.
  • Wenn myObjectes sich um einen Zeiger auf ein lokales stapelbasiertes Objekt handelt, ist das Ergebnis undefiniert.
  • Wenn myObjectes sich um einen Zeiger auf ein Element / globales Objekt handelt, wird dieser Zeiger zurückgegeben.
  • Wenn myObjectes sich um einen Zeiger auf ein Heap-basiertes Objekt handelt, wird dieser Zeiger zurückgegeben.

Was macht nun derselbe Code in Java?

return myObject;
  • Der Verweis auf myObjectwird zurückgegeben. Es spielt keine Rolle, ob die Variable lokal, ein Mitglied oder global ist. und es gibt keine stapelbasierten Objekte oder Zeigerfälle, über die man sich Sorgen machen müsste.

Das Obige zeigt, warum stapelbasierte Objekte eine sehr häufige Ursache für Programmierfehler in C ++ sind. Aus diesem Grund haben die Java-Designer sie herausgenommen. und ohne sie macht es keinen Sinn, RAII in Java zu verwenden.

BlueRaja - Danny Pflughoeft
quelle
6
Ich weiß nicht, was Sie mit "es gibt keinen Grund für RAII" meinen ... Ich denke, Sie meinen "es gibt keine Möglichkeit, RAII in Java bereitzustellen" ... RAII ist unabhängig von einer Sprache ... das ist nicht der Fall "sinnlos" werden, weil 1 bestimmte Sprache es nicht bietet
JoelFan
4
Das ist kein triftiger Grund. Ein Objekt muss nicht unbedingt auf dem Stapel gespeichert sein, um stapelbasiertes RAII verwenden zu können. Wenn es so etwas wie eine "eindeutige Referenz" gibt, kann der Destruktor ausgelöst werden, sobald er den Gültigkeitsbereich verlässt. Sehen Sie zum Beispiel, wie es mit der Programmiersprache D funktioniert: d-programming-language.org/exception-safe.html
Nemanja Trifunovic
3
@ Nemanja: Ein Objekt muss nicht auf dem Stapel gespeichert sein , um eine stapelbasierte Semantik zu haben, und ich habe nie gesagt, dass dies der Fall ist . Aber das ist nicht das Problem. Das Problem ist, wie bereits erwähnt, die stapelbasierte Semantik.
BlueRaja - Danny Pflughoeft
4
@Aaronaught: Der Teufel ist in "fast immer" und "die meiste Zeit". Wenn Sie Ihre db-Verbindung nicht schließen und dem GC überlassen, um den Finalizer auszulösen, funktioniert dies einwandfrei mit Ihren Komponententests und führt bei der Bereitstellung in der Produktion zu schweren Brüchen. Deterministische Aufräumarbeiten sind unabhängig von der Sprache wichtig.
Nemanja Trifunovic
8
@NemanjaTrifunovic: Warum testen Sie Einheiten in einer Live-Datenbankverbindung? Das ist eigentlich kein Unit Test. Nein, tut mir leid, ich kaufe es nicht. Sie sollten ohnehin nicht überall DB-Verbindungen erstellen, sondern diese über Konstruktoren oder Eigenschaften übergeben. Das bedeutet, dass Sie keine stapelähnliche Semantik für die automatische Destruktion wünschen. Sehr wenige Objekte, die von einer Datenbankverbindung abhängig sind, sollten diese tatsächlich besitzen. Wenn Sie die nicht deterministische Bereinigung so oft und so hart trifft, dann liegt das an einem schlechten Anwendungsdesign, nicht an einem schlechten Sprachdesign.
Aaronaught
17

Ihre Beschreibung der Löcher von usingist unvollständig. Betrachten Sie das folgende Problem:

interface Bar {
    ...
}
class Foo : Bar, IDisposable {
    ...
}

Bar b = new Foo();

// Where's the Dispose?

Meiner Meinung nach war es eine schlechte Idee, weder RAII noch GC zu haben. Wenn es darum geht, Dateien in Java zu schließen, ist es malloc()und free()dort drüben.

DeadMG
quelle
2
Ich stimme zu, dass RAII die Bienenknie sind. Die usingKlausel ist jedoch ein großer Fortschritt für C # über Java. Es ermöglicht eine deterministische Zerstörung und damit ein korrektes Ressourcenmanagement (es ist nicht ganz so gut wie RAII, an das Sie sich erinnern müssen, aber es ist definitiv eine gute Idee).
Martin York
8
"Wenn es darum geht, Dateien in Java zu schließen, ist es dort malloc () und free ()." - Absolut.
Konrad Rudolph
9
@KonradRudolph: Es ist schlimmer als malloc und kostenlos. Zumindest in C gibt es keine Ausnahmen.
Nemanja Trifunovic
1
@Nemanja: Lass uns fair sein, das kannst du free()in der finally.
DeadMG
4
@Loki: Das Basisklassenproblem ist als Problem viel wichtiger. Beispielsweise hat das Original IEnumerablenicht von geerbt IDisposable, und es gab eine Reihe von speziellen Iteratoren, die niemals implementiert werden konnten.
DeadMG
14

Ich bin ziemlich alt. Ich war dort und habe es gesehen und mir oft den Kopf gestoßen.

Ich war auf einer Konferenz in Hursley Park, wo die IBM-Jungs uns erzählten, wie wunderbar diese brandneue Java-Sprache war. Nur jemand fragte ... warum gibt es keinen Destruktor für diese Objekte? Er meinte nicht das, was wir als Destruktor in C ++ kennen, aber es gab auch keinen Finalizer (oder es gab Finalizer, aber sie funktionierten im Grunde nicht). Dies ist weit zurück und wir haben festgestellt, dass Java zu diesem Zeitpunkt eine Art Spielzeugsprache war.

Jetzt haben sie Finalisers in die Sprachspezifikation aufgenommen und Java hat einige Änderungen erfahren.

Natürlich wurde später jeder angewiesen, keine Finalisten auf seine Objekte zu setzen, da dies den GC enorm verlangsamte. (da nicht nur der Heap gesperrt, sondern auch die zu finalisierenden Objekte in einen temporären Bereich verschoben werden mussten, da diese Methoden nicht aufgerufen werden konnten, da der GC die Ausführung der App angehalten hat. Stattdessen wurden sie unmittelbar vor dem nächsten aufgerufen.) GC-Zyklus) (und schlimmer noch, manchmal wurde der Finalizer beim Herunterfahren der App überhaupt nicht aufgerufen. Stellen Sie sich vor, Sie hätten Ihr Datei-Handle niemals geschlossen.)

Dann hatten wir C # und ich erinnere mich an das Diskussionsforum auf MSDN, in dem uns gesagt wurde, wie wunderbar diese neue C # -Sprache war. Jemand fragte, warum es keine deterministische Finalisierung gebe und die MS-Jungs sagten uns, dass wir solche Dinge nicht brauchten, dann sagten sie uns, wir müssten unsere Art, Apps zu entwerfen, ändern, und erklärten uns dann, wie großartig GC sei und wie alle unsere alten Apps seien Quatsch und hat wegen aller Zirkelverweise nie funktioniert. Dann gaben sie dem Druck nach und sagten uns, dass sie dieses IDispose-Muster zu der Spezifikation hinzugefügt hätten, die wir verwenden könnten. Ich dachte, dass es zu diesem Zeitpunkt für uns in C # -Anwendungen so ziemlich wieder manuelle Speicherverwaltung war.

Natürlich stellten die MS-Jungs später fest, dass alles, was sie uns erzählt hatten, ... nun, sie haben IDispose ein bisschen mehr als nur eine Standardschnittstelle gemacht und später die using-Anweisung hinzugefügt. W00t! Sie erkannten, dass die deterministische Finalisierung doch etwas in der Sprache fehlte. Natürlich muss man immer noch daran denken, es überall einzulegen, also ist es immer noch ein bisschen manuell, aber es ist besser.

Warum haben sie es dann getan, wenn sie von Anfang an automatisch eine Semantik nach Verwendungsstil in jeden Gültigkeitsbereichsblock eingefügt haben könnten? Wahrscheinlich Effizienz, aber ich denke gerne, dass sie es einfach nicht gemerkt haben. Genau wie sie irgendwann gemerkt haben, dass Sie in .NET noch intelligente Zeiger benötigen (google SafeHandle), dachten sie, dass der GC wirklich alle Probleme lösen würde. Sie vergaßen, dass ein Objekt mehr als nur Speicher ist und dass GC in erster Linie für die Speicherverwaltung entwickelt wurde. Sie waren in die Idee verwickelt, dass der GC damit umgehen würde, und vergaßen, dass Sie andere Dinge darin ablegen. Ein Objekt ist nicht nur ein Gedächtnisfleck, der keine Rolle spielt, wenn Sie es für eine Weile nicht löschen.

Aber ich denke auch, dass das Fehlen einer Finalisierungsmethode im ursprünglichen Java ein bisschen mehr zu tun hatte - dass die Objekte, die Sie erstellt haben, sich ausschließlich um den Arbeitsspeicher drehten und ob Sie etwas anderes löschen wollten (wie ein DB-Handle oder einen Socket oder was auch immer) ) dann sollten Sie es manuell tun .

Denken Sie daran, Java wurde für eingebettete Umgebungen entwickelt, in denen die Benutzer C-Code mit vielen manuellen Zuweisungen geschrieben haben. Das automatische Freigeben war also kein großes Problem - sie haben es zuvor noch nie getan. Warum sollten Sie es in Java benötigen? Das Problem hatte nichts mit Threads oder Stack / Heap zu tun, sondern war wahrscheinlich nur dazu da, die Speicherzuweisung (und damit die Aufhebung der Zuweisung) etwas zu vereinfachen. Insgesamt ist die try / finally-Anweisung wahrscheinlich ein besserer Ort, um mit Nicht-Speicherressourcen umzugehen.

Also meiner Meinung nach ist die Art und Weise, wie .NET den größten Fehler von Java kopiert, seine größte Schwäche. .NET hätte ein besseres C ++ sein sollen, kein besseres Java.

gbjbaanb
quelle
IMHO, Dinge wie "using" -Blöcke sind der richtige Ansatz zur deterministischen Bereinigung, aber es sind noch einige weitere Dinge erforderlich: (1) ein Mittel, um sicherzustellen, dass Objekte entsorgt werden, wenn ihre Destruktoren eine Ausnahme auslösen; (2) ein Mittel zum automatischen Erzeugen einer Routinemethode zum Aufrufen Disposealler mit einer usingDirektive markierten Felder und zum Festlegen, ob diese IDisposable.Disposeautomatisch aufgerufen werden soll; (3) eine ähnliche Richtlinie using, die jedoch nur Disposeim Falle einer Ausnahme gelten würde; (4) eine Variation IDisposabledavon würde einen ExceptionParameter annehmen , und ...
Superkatze
... die gegebenenfalls automatisch verwendet werden using; Der Parameter würde lauten, nullwenn der usingBlock normal beendet wird, oder würde angeben, welche Ausnahme ansteht, wenn er über eine Ausnahme beendet wird. Wenn es solche Dinge gäbe, wäre es viel einfacher, Ressourcen effektiv zu verwalten und Lecks zu vermeiden.
Supercat
11

Bruce Eckel, Autor von "Thinking in Java" und "Thinking in C ++" und Mitglied des C ++ - Standardisierungsausschusses, ist der Meinung, dass Gosling und das Java-Team in vielen Bereichen (nicht nur RAII) nicht ihre Arbeit geleistet haben Hausaufgaben.

... Um zu verstehen, wie die Sprache sowohl unangenehm als auch kompliziert und gleichzeitig gut gestaltet sein kann, müssen Sie die primäre Entwurfsentscheidung berücksichtigen, auf die sich alles in C ++ festgelegt hat: Kompatibilität mit C. Stroustrup - und das zu Recht Es sieht so aus, als ob die Massen von C-Programmierern dazu gebracht werden müssten, sich auf Objekte zu bewegen, um die Bewegung transparent zu machen: um ihnen zu ermöglichen, ihren C-Code unverändert unter C ++ zu kompilieren. Dies war eine enorme Einschränkung und war schon immer die größte Stärke von C ++ ... und ihr Fluch. Es ist das, was C ++ so erfolgreich wie es war und so komplex wie es ist.

Es hat auch die Java-Designer zum Narren gehalten, die C ++ nicht gut genug verstanden haben. Zum Beispiel hielten sie das Überladen von Operatoren für zu schwierig, um es von Programmierern richtig zu nutzen. Dies gilt im Grunde genommen für C ++, da C ++ sowohl Stapelzuweisung als auch Heapzuweisung hat und Sie Ihre Operatoren überlasten müssen, um alle Situationen zu bewältigen und keine Speicherverluste zu verursachen. In der Tat schwierig. Java verfügt jedoch über einen einzigen Speicherzuordnungsmechanismus und einen Garbage Collector, wodurch das Überladen von Operatoren trivial wird - wie in C # gezeigt (aber bereits in Python vor Java gezeigt). Viele Jahre lang lautete die teilweise Aussage des Java-Teams: "Das Überladen von Operatoren ist zu kompliziert." Diese und viele andere Entscheidungen, bei denen eindeutig jemand

Es gibt viele andere Beispiele. Primitive "mussten für die Effizienz enthalten sein." Die richtige Antwort ist, "alles ist ein Objekt" treu zu bleiben und eine Falltür für Aktivitäten auf niedrigerer Ebene bereitzustellen, wenn Effizienz erforderlich war (dies hätte es auch ermöglicht, dass die Hotspot-Technologien die Dinge transparenter effizienter machen, als sie es letztendlich tun würden haben). Oh, und die Tatsache, dass Sie den Fließkommaprozessor nicht direkt zur Berechnung transzendenter Funktionen verwenden können (dies geschieht stattdessen in Software). Ich habe so viel wie möglich über solche Themen geschrieben, und die Antwort, die ich höre, war immer eine tautologische Antwort darauf, dass "dies der Java-Weg ist".

Als ich darüber schrieb, wie schlecht Generics entworfen wurden, erhielt ich die gleiche Antwort, zusammen mit "Wir müssen mit früheren (schlechten) Entscheidungen in Java abwärtskompatibel sein". In letzter Zeit haben immer mehr Leute genug Erfahrung mit Generics gesammelt, um zu sehen, dass sie wirklich sehr schwer zu verwenden sind - tatsächlich sind C ++ - Vorlagen viel leistungsfähiger und konsistenter (und jetzt, da Compiler-Fehlermeldungen tolerierbar sind, viel einfacher zu verwenden). Die Leute haben sogar die Verdinglichung ernst genommen - etwas, das hilfreich wäre, aber das Design, das durch selbst auferlegte Zwänge verkrüppelt ist, nicht sonderlich beeinträchtigt.

Die Liste geht bis zu dem Punkt, an dem es einfach langweilig ist ...

Gnawme
quelle
5
Dies klingt eher nach einer Java- oder C ++ - Antwort als nach einer RAII-Antwort. Ich denke, C ++ und Java sind verschiedene Sprachen, jede mit ihren Stärken und Schwächen. Auch die C ++ - Designer haben in vielen Bereichen ihre Hausaufgaben nicht gemacht (KISS-Prinzip nicht angewendet, einfacher Importmechanismus für Klassen fehlt usw.). Der Fokus der Frage lag jedoch auf RAII: Dies fehlt in Java und muss manuell programmiert werden.
Giorgio
4
@ Giorgio: Der Punkt des Artikels ist, Java scheint das Boot in einer Reihe von Fragen verpasst zu haben, von denen einige direkt mit RAII zusammenhängen. In Bezug auf C ++ und seine Auswirkungen auf Java bemerkt Eckels: "Sie müssen die primäre Entwurfsentscheidung berücksichtigen, auf die sich alles in C ++ gestützt hat: Kompatibilität mit C. Dies war eine enorme Einschränkung und war schon immer die größte Stärke von C ++ ... und seine Es hat auch die Java-Designer zum Narren gehalten, die C ++ nicht gut genug verstanden haben. " Das Design von C ++ beeinflusste Java direkt, während C # die Möglichkeit hatte, von beiden zu lernen. (Ob es so war, ist eine andere Frage.)
Gnawme
2
@Giorgio Das Studium bestehender Sprachen in einem bestimmten Paradigma und einer bestimmten Sprachfamilie ist in der Tat ein Teil der Hausaufgaben, die für die Entwicklung neuer Sprachen erforderlich sind. Dies ist ein Beispiel, wo sie es einfach mit Java gewischt haben. Sie hatten C ++ und Smalltalk zum Anschauen. Bei der Entwicklung von C ++ musste Java nicht beachtet werden.
Jeremy
1
@ Gnawme: "Java scheint das Boot in einigen Punkten verpasst zu haben, von denen einige direkt mit RAII zusammenhängen." Können Sie diese Punkte erwähnen? In dem Artikel, den Sie veröffentlicht haben, wird RAII nicht erwähnt.
Giorgio
2
@Giorgio Sicher, es gibt seit der Entwicklung von C ++ Neuerungen, die viele der dort fehlenden Funktionen erklären. Sind irgendwelche jener Eigenschaften , die sie sollten vor der Entwicklung von C ++ etabliert haben festgestellt , bei Sprachen suchen? Das ist die Art von Hausaufgabe, über die wir mit Java sprechen - es gibt keinen Grund für sie, nicht jedes C ++ - Feature in der Entwicklung von Java zu berücksichtigen. Einige mögen Mehrfachvererbung, die sie absichtlich ausgelassen haben - andere wie RAII scheinen sie übersehen zu haben.
Jeremy
10

Der beste Grund ist viel einfacher als die meisten Antworten hier.

Sie können stapelzugeordnete Objekte nicht an andere Threads übergeben.

Halte inne und denke darüber nach. Denken Sie weiter ... Jetzt hatte C ++ keine Threads, als alle so scharf auf RAII waren. Sogar Erlang (separate Haufen pro Thread) wird ickelig, wenn Sie zu viele Objekte herumreichen. C ++ hat in C ++ 2011 nur ein Speichermodell; Jetzt können Sie fast über Parallelität in C ++ nachdenken, ohne auf die "Dokumentation" Ihres Compilers verweisen zu müssen.

Java wurde ab (fast) dem ersten Tag für mehrere Threads entwickelt.

Ich habe immer noch meine alte Version von "The C ++ Programming Language", in der Stroustrup mir versichert, dass ich keine Threads benötige.

Der zweite schmerzhafte Grund ist, das Schneiden zu vermeiden.

Tim Williscroft
quelle
1
Wenn Java für mehrere Threads entwickelt wurde, wird auch erklärt, warum der GC nicht auf Referenzzählung basiert.
dan04
4
@NemanjaTrifunovic: Sie können C ++ / CLI nicht mit Java oder C # vergleichen. Es wurde fast ausschließlich für die Zusammenarbeit mit nicht verwaltetem C / C ++ - Code entwickelt. Es ist eher eine nicht verwaltete Sprache, die zufällig Zugriff auf das .NET-Framework gewährt, als umgekehrt.
Aaronaught
3
@NemanjaTrifunovic: Ja, C ++ / CLI ist ein Beispiel dafür, wie dies auf eine Weise geschehen kann, die für normale Anwendungen völlig ungeeignet ist . Es ist nur für C / C ++ - Interop nützlich. Normale Entwickler sollten nicht nur nicht mit einer völlig irrelevanten "Stack- oder Heap" -Entscheidung konfrontiert sein müssen, sondern es ist trivial einfach, versehentlich einen Nullzeiger- / Referenzfehler und / oder ein Speicherverlust zu erzeugen, wenn Sie jemals versuchen, ihn zu refaktorisieren. Tut mir leid, aber ich muss mich fragen, ob Sie jemals tatsächlich in Java oder C # programmiert haben, denn ich glaube nicht, dass jemand , der dies getan hat, die in C ++ / CLI verwendete Semantik wirklich haben möchte .
Aaronaught
2
@Aaronaught: Ich habe sowohl mit Java (ein wenig) als auch mit C # (viel) programmiert und mein aktuelles Projekt ist so ziemlich alles mit C #. Glauben Sie mir, ich weiß, wovon ich spreche, und es hat nichts mit "Stack vs. Heap" zu tun - es hat alles damit zu tun, sicherzustellen, dass alle Ihre Ressourcen freigegeben werden, sobald Sie sie nicht mehr benötigen. Automatisch. Wenn nicht, werden Sie in Schwierigkeiten geraten.
Nemanja Trifunovic
4
@NemanjaTrifunovic: Das ist großartig, wirklich großartig, aber sowohl in C # als auch in C ++ / CLI müssen Sie explizit angeben, wann dies geschehen soll. Sie verwenden lediglich eine andere Syntax. Niemand bestreitet das Wesentliche, worüber Sie gerade streiten (dass "Ressourcen freigegeben werden, sobald Sie sie nicht benötigen"), aber Sie machen einen gigantischen logischen Sprung zu "alle verwalteten Sprachen sollten automatisch aber nur verfügbar sein -sort-of-call-stack-based deterministic entsorgung ". Es hält einfach kein Wasser.
Aaronaught
5

In C ++ verwenden Sie allgemeinere, untergeordnete Sprachfunktionen (Destruktoren, die automatisch für stapelbasierte Objekte aufgerufen werden), um eine übergeordnete (RAII) zu implementieren, und dieser Ansatz scheint den C # / Java-Leuten nicht zu eigen zu sein zu gern. Sie möchten lieber spezielle High-Level-Tools für bestimmte Anforderungen entwerfen und diese den Programmierern bereitstellen, die in die Sprache integriert sind. Das Problem mit solchen spezifischen Tools ist, dass sie oft nicht angepasst werden können (was sie teilweise so einfach zu erlernen macht). Wenn Sie aus kleineren Blöcken bauen, könnte sich mit der Zeit eine bessere Lösung ergeben, während dies weniger wahrscheinlich ist , wenn Sie nur über integrierte Konstrukte auf hoher Ebene verfügen.

Also ja, ich denke (ich war eigentlich nicht da ...), es war eine bewusste Entscheidung mit dem Ziel, die Sprachen leichter zu erlernen, aber meiner Meinung nach war es eine schlechte Entscheidung. Andererseits bevorzuge ich im Allgemeinen, dass C ++ den Programmierern die Chance gibt, ihre eigene Philosophie zu entwickeln, also bin ich ein bisschen voreingenommen.

imre
quelle
7
Die "Geben-Sie-den-Programmierern-die-Chance-,-ihre-eigene-Philosophie-zu-rollen" funktioniert einwandfrei, BIS Sie Bibliotheken kombinieren müssen, die von Programmierern geschrieben wurden, die jeweils ihre eigenen Zeichenfolgenklassen und intelligenten Zeiger gerollt haben.
dan04
@ dan04 also die verwalteten Sprachen, die Ihnen vordefinierte Zeichenfolgenklassen geben, dann können Sie sie mit Affen patchen, was ein Rezept für eine Katastrophe ist, wenn Sie der Typ sind, der mit einer anderen selbst gerollten Zeichenfolge nicht zurechtkommt Klasse.
Gbjbaanb
-1

Das grobe Äquivalent dazu haben Sie bereits in C # mit der DisposeMethode aufgerufen . Java hat auch finalize. HINWEIS: Mir ist klar, dass das Finalisieren von Java nicht deterministisch ist und sich von unterscheidet Dispose. Ich möchte nur darauf hinweisen, dass beide neben dem GC eine Methode zum Reinigen von Ressourcen haben.

Wenn überhaupt, wird C ++ mehr zum Schmerz, weil ein Objekt physisch zerstört werden muss. In höheren Sprachen wie C # und Java sind wir auf einen Garbage Collector angewiesen, um ihn zu bereinigen, wenn keine Referenzen mehr vorhanden sind. Es gibt keine Garantie dafür, dass das DBConnection-Objekt in C ++ keine falschen Verweise oder Zeiger darauf enthält.

Ja, der C ++ - Code kann intuitiver zu lesen sein, aber ein Albtraum beim Debuggen sein, da die Grenzen und Einschränkungen, die Sprachen wie Java setzen, einige der erschwerenderen und schwierigeren Fehler ausschließen und andere Entwickler vor häufigen Anfängerfehlern schützen.

Vielleicht liegt es an den Vorlieben, von denen einige die Leistung, Kontrolle und Reinheit von C ++ auf niedriger Ebene mögen, während andere wie ich eine Sandbox-Sprache bevorzugen, die viel expliziter ist.

maple_shaft
quelle
12
Erstens ist Javas "finalize" nicht deterministisch ... es ist nicht das Äquivalent von C # 's "dispose" oder von C ++' s Destruktoren ... C ++ hat auch einen Garbage Collector, wenn Sie .NET
JoelFan
2
@DeadMG: Das Problem ist, dass Sie vielleicht kein Idiot sind, aber dieser andere Typ, der gerade die Firma verlassen hat (und den Code geschrieben hat, den Sie jetzt pflegen), könnte es gewesen sein.
Kevin
7
Dieser Kerl wird beschissenen Code schreiben, was immer Sie tun. Sie können einen schlechten Programmierer nicht dazu bringen, guten Code zu schreiben. Baumelnde Zeiger sind das geringste Problem für mich im Umgang mit Idioten. Gute Codierungsstandards verwenden intelligente Zeiger für den Speicher, der nachverfolgt werden muss. Daher sollte bei der intelligenten Verwaltung deutlich gemacht werden, wie der Speicher sicher freigegeben und auf ihn zugegriffen werden kann.
DeadMG
3
Was DeadMG gesagt hat. Es gibt viele schlechte Dinge über C ++. Aber RAII ist noch lange nicht einer von ihnen. Tatsächlich ist das Fehlen von Java und .NET für die ordnungsgemäße Berücksichtigung der Ressourcenverwaltung (weil der Speicher die einzige Ressource ist, oder?) Eines ihrer größten Probleme.
Konrad Rudolph
8
Der Finalizer ist meiner Meinung nach ein Desaster-Design. Da Sie die korrekte Verwendung eines Objekts vom Designer auf den Benutzer des Objekts übertragen (nicht im Hinblick auf die Speicherverwaltung, sondern auf die Ressourcenverwaltung). In C ++ liegt es in der Verantwortung des Klassendesigners, die Ressourcenverwaltung korrekt durchzuführen (nur einmal). In Java liegt es in der Verantwortung des Klassenbenutzers, die Ressourcenverwaltung korrekt durchzuführen, und muss daher jedes Mal durchgeführt werden, wenn die Klasse von uns verwendet wird. stackoverflow.com/questions/161177/…
Martin York