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 DbConnection
in einem using
Block 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 DbConnection
hat
class Foo {
DbConnection dbConn;
// ...
}
In C # müsste ich Foo IDisposable
als solches implementieren :
class Foo : IDisposable {
DbConnection dbConn;
public void Dispose()
{
dbConn.Dispose();
}
}
und was noch schlimmer ist, jeder Benutzer von Foo
müsste daran denken, Foo
in einem using
Block 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?)
quelle
Antworten:
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
quelle
manual
Memory Management nicht nennen . Sie sind eher wie ein deterministischer, feinkörniger, steuerbarer Müllsammler. Bei richtiger Anwendung sind intelligente Zeiger die Bienenknie.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
using
Schlüsselwort, das ziemlich spät im Entwurf von C # 1.0 eingegangen ist. DasIDisposable
Ganze ist ziemlich elend, und man fragt sich, ob es eleganter wäre,using
mit der C ++ - Destruktorsyntax~
diejenigen Klassen zu kennzeichnen, auf die das Boiler-Plate-IDisposable
Muster automatisch angewendet werden könnte.quelle
~
Syntax als syntaktischen Zucker fürIDisposable.Dispose()
~
als syntaktischer Zucker fürIDisposable.Dispose()
, und es ist viel bequemer als die C # Syntax.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.
Foo
undFoo*
oder zwischenfoo.bar
und ist nicht erforderlichfoo->bar
.clone()
. Viele Objekte müssen nur nicht kopiert werden. Zum Beispiel immutables dies nicht tun.)private
Kopierkonstruktoren zu deklarieren undoperator=
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.swap
Funktionen benötigt. (Es sei denn, Sie schreiben eine Sortierroutine.)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ür
Dispose
( Original-Link ), ähnlich wie C #using
. Es löst jedoch nicht das allgemeine Problem der deterministischen Zerstörung, da Sie eine anonyme erstellen könnengcnew FileStream("filename.ext")
und C ++ / CLI diese nicht automatisch entsorgt.quelle
using
Anweisung 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", undIDisposable
solchen, die dies nicht tun. Das Überschreiben oder Verlassen eines Speicherorts, der einen referenzierten Speicherort besitzt,IDisposable
sollte das Ziel in Ermangelung einer gegenteiligen Richtlinie veranlassen.new Date(oldDate.getTime())
.Java7 hat etwas Ähnliches wie C # eingeführt
using
: Die Anweisung try-with-resourcesIch denke, sie haben sich entweder nicht bewusst dafür entschieden, RAII nicht zu implementieren, oder sie haben es sich inzwischen anders überlegt.
quelle
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 lassenAutoCloseable
...using
ist 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.using
/ try-with-resources stimmt nicht mit RAII überein.using
und es ist ilk sind bei weitem nicht RAII.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?
myObject
es sich um ein lokales stapelbasiertes Objekt handelt, wird der Kopierkonstruktor aufgerufen (wenn das Ergebnis einer bestimmten Funktion zugewiesen ist).myObject
es sich um ein lokales stapelbasiertes Objekt handelt und wir eine Referenz zurückgeben, ist das Ergebnis undefiniert.myObject
es sich um ein Element / globales Objekt handelt, wird der Kopierkonstruktor aufgerufen (wenn das Ergebnis einer bestimmten Funktion zugewiesen ist).myObject
es sich um ein Element / globales Objekt handelt und wir eine Referenz zurückgeben, wird die Referenz zurückgegeben.myObject
es sich um einen Zeiger auf ein lokales stapelbasiertes Objekt handelt, ist das Ergebnis undefiniert.myObject
es sich um einen Zeiger auf ein Element / globales Objekt handelt, wird dieser Zeiger zurückgegeben.myObject
es sich um einen Zeiger auf ein Heap-basiertes Objekt handelt, wird dieser Zeiger zurückgegeben.Was macht nun derselbe Code in Java?
myObject
wird 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.
quelle
Ihre Beschreibung der Löcher von
using
ist unvollständig. Betrachten Sie das folgende Problem: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()
undfree()
dort drüben.quelle
using
Klausel 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).free()
in derfinally
.IEnumerable
nicht von geerbtIDisposable
, und es gab eine Reihe von speziellen Iteratoren, die niemals implementiert werden konnten.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.
quelle
Dispose
aller mit einerusing
Direktive markierten Felder und zum Festlegen, ob dieseIDisposable.Dispose
automatisch aufgerufen werden soll; (3) eine ähnliche Richtlinieusing
, die jedoch nurDispose
im Falle einer Ausnahme gelten würde; (4) eine VariationIDisposable
davon würde einenException
Parameter annehmen , und ...using
; Der Parameter würde lauten,null
wenn derusing
Block 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.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.
quelle
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.
quelle
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.
quelle
Das grobe Äquivalent dazu haben Sie bereits in C # mit der
Dispose
Methode aufgerufen . Java hat auchfinalize
. HINWEIS: Mir ist klar, dass das Finalisieren von Java nicht deterministisch ist und sich von unterscheidetDispose
. 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.
quelle