Warum fehlt das Paradigma des Objektdestruktors in Sprachen, die durch Müll gesammelt wurden?


27

Auf der Suche nach Einsichten in Entscheidungen rund um müllsammelndes Sprachdesign. Vielleicht könnte mich ein Sprachexperte aufklären? Ich komme aus einem C ++ - Hintergrund, daher ist dieser Bereich für mich verwirrend.

Es scheint, dass fast alle modernen Sprachen mit OOPy-Objektunterstützung wie Ruby, Javascript / ES6 / ES7, Actionscript, Lua usw. das Destruktor- / Finalisierungsparadigma vollständig weglassen. Python scheint die einzige mit seiner class __del__()Methode zu sein. Warum ist das? Gibt es funktionale / theoretische Einschränkungen in Sprachen mit automatischer Speicherbereinigung, die eine effektive Implementierung einer Destruktor- / Finalisierungsmethode für Objekte verhindern?

Ich finde es äußerst mangelhaft, dass diese Sprachen das Gedächtnis als die einzige Ressource betrachten, die es wert ist, verwaltet zu werden. Was ist mit Sockets, Dateizugriffsnummern und Anwendungsstatus? Ohne die Möglichkeit, benutzerdefinierte Logik zum Bereinigen von Nicht-Speicherressourcen und Zuständen bei der Objekt-Finalisierung zu implementieren, muss ich meine Anwendung mit benutzerdefinierten myObject.destroy()Stilaufrufen überschütten, die Bereinigungslogik außerhalb meiner "Klasse" platzieren, den Verkapselungsversuch unterbrechen und meine ablegen Anwendung auf Ressourcenlecks aufgrund von menschlichem Versagen anstatt automatisch vom gc behandelt zu werden.

Was sind die Entscheidungen zum Sprachdesign, die dazu führen, dass diese Sprachen keine Möglichkeit haben, benutzerdefinierte Logik für die Objektentsorgung auszuführen? Ich muss mir vorstellen, dass es einen guten Grund gibt. Ich möchte die technischen und theoretischen Entscheidungen, die dazu geführt haben, dass diese Sprachen die Zerstörung / Finalisierung von Objekten nicht unterstützen, besser verstehen.

Aktualisieren:

Vielleicht eine bessere Möglichkeit, meine Frage zu formulieren:

Warum sollte eine Sprache das eingebaute Konzept von Objektinstanzen mit Klassen oder klassenähnlichen Strukturen zusammen mit benutzerdefinierten Instanzen (Konstruktoren) haben und dennoch die Zerstörungs- / Finalisierungsfunktionalität vollständig weglassen? Sprachen, die eine automatische Speicherbereinigung anbieten, scheinen die Hauptkandidaten für die Zerstörung / Finalisierung von Objekten zu sein, da sie mit 100% iger Sicherheit wissen, wann ein Objekt nicht mehr verwendet wird. Die meisten dieser Sprachen unterstützen dies jedoch nicht.

Ich denke nicht, dass es ein Fall ist, in dem der Destruktor niemals aufgerufen wird, da dies ein Kernspeicherverlust wäre, den gcs vermeiden soll. Ich konnte ein mögliches Argument dafür sehen, dass der Destruktor / Finalizer möglicherweise erst zu einem unbestimmten Zeitpunkt in der Zukunft aufgerufen wird, aber das hat Java oder Python nicht davon abgehalten, die Funktionalität zu unterstützen.

Was sind die Hauptgründe für das Design von Sprachen, um keine Form der Objekt-Finalisierung zu unterstützen?


9
Vielleicht, weil finalize/ destroyeine Lüge ist? Es gibt keine Garantie, dass es jemals ausgeführt wird. Und selbst wenn Sie nicht wissen, wann (bei gegebener automatischer Garbage Collection) und falls erforderlich, immer noch Kontext vorhanden ist (möglicherweise wurde er bereits gesammelt). Es ist also sicherer, einen konsistenten Zustand auf andere Weise sicherzustellen, und man möchte den Programmierer möglicherweise dazu zwingen.
Raphael

1
Ich denke, diese Frage ist grenzwertig. Handelt es sich um eine Designfrage für Programmiersprachen, die wir unterhalten möchten, oder handelt es sich um eine Frage für eine Site, die sich mehr an der Programmierung orientiert? Gemeinschaftsstimmen, bitte.
Raphael

14
Es ist eine gute Frage im PL-Design, lassen Sie es uns haben.
Andrej Bauer

3
Dies ist keine statische / dynamische Unterscheidung. Viele statische Sprachen haben keine Finalizer. Sind Sprachen mit Finalisierern nicht in der Minderheit?
Andrej Bauer

1
Ich denke, hier gibt es einige Fragen ... Es wäre besser, wenn Sie Begriffe ein bisschen mehr definieren. java hat einen finally-Block, der nicht mit der Zerstörung von Objekten verknüpft ist, sondern mit dem Methoden-Exit. Es gibt auch andere Möglichkeiten, mit Ressourcen umzugehen. In Java kann ein Verbindungspool z. B. Verbindungen verwalten, die [x] nicht verwendet werden, und sie zurückfordern. nicht elegant, aber es funktioniert. Ein Teil der Antwort auf Ihre Frage ist, dass die Speicherbereinigung in etwa ein nicht deterministischer, nicht augenblicklicher Prozess ist und nicht von Objekten gesteuert wird, die nicht mehr verwendet werden, sondern von Speicherbeschränkungen / -obergrenzen, die ausgelöst werden.
22.

Antworten:


10

Das Muster, über das Sie sprechen, bei dem Objekte wissen, wie sie ihre Ressourcen bereinigen, lässt sich in drei relevante Kategorien unterteilen. Lassen Sie uns Destruktoren nicht mit Finalisierern in Konflikt bringen - nur eine ist mit der Garbage Collection verbunden:

  • Die vom Programmierer automatisch festgelegte Aufräummethode finalizer pattern : cleanup wird automatisch aufgerufen.

    Finalizer werden vor der Freigabe durch einen Garbage Collector automatisch aufgerufen. Der Begriff gilt, wenn der verwendete Speicherbereinigungsalgorithmus Objektlebenszyklen bestimmen kann.

  • Die vom Programmierer automatisch deklarierte Bereinigungsmethode destructor pattern : wird nur manchmal automatisch aufgerufen.

    Destruktoren können für vom Stapel zugewiesene Objekte automatisch aufgerufen werden (da die Objektlebensdauer deterministisch ist), müssen jedoch explizit für alle möglichen Ausführungspfade für vom Heap zugewiesene Objekte aufgerufen werden (da die Objektlebensdauer nicht deterministisch ist).

  • Die vom Programmierer deklarierte, definierte und aufgerufene Bereinigungsmethode " disposer pattern : cleanup".

    Programmierer erstellen eine Entsorgungsmethode und nennen sie selbst - hier liegt Ihre benutzerdefinierte myObject.destroy()Methode. Wenn eine Entsorgung unbedingt erforderlich ist, müssen die Entsorger auf allen möglichen Ausführungswegen angerufen werden.

Finalizer sind die Droiden, nach denen Sie suchen.

Das Finalizer-Muster (das Muster, nach dem Sie fragen) ist der Mechanismus zum Zuordnen von Objekten zu Systemressourcen (Sockets, Dateideskriptoren usw.) für die gegenseitige Wiederherstellung durch einen Garbage Collector. Finalizer sind jedoch grundsätzlich dem verwendeten Garbage Collection-Algorithmus ausgeliefert.

Betrachten Sie diese Annahme von Ihnen:

Sprachen, die automatische Speicherbereinigung bieten ... wissen mit 100% iger Sicherheit, wann ein Objekt nicht mehr verwendet wird.

Technisch falsch (danke, @babou). Bei der Garbage Collection geht es im Wesentlichen um Speicher, nicht um Objekte. Ob oder wann ein Erfassungsalgorithmus feststellt, dass der Speicher eines Objekts nicht mehr verwendet wird, hängt vom Algorithmus und (möglicherweise) davon ab, wie sich Ihre Objekte aufeinander beziehen. Lassen Sie uns über zwei Arten von Laufzeit-Garbage-Collectors sprechen. Es gibt viele Möglichkeiten, diese zu grundlegenden Techniken zu ändern und zu erweitern:

  1. GC verfolgen. Diese verfolgen Speicher, keine Objekte. Sofern dies nicht erweitert wird, behalten sie keine Rückverweise auf Objekte aus dem Speicher bei. Sofern nicht erweitert, wissen diese GCs nicht, wann ein Objekt finalisiert werden kann, auch wenn sie wissen, wann sein Speicher nicht erreichbar ist. Daher können Finalizer-Aufrufe nicht garantiert werden.

  2. Referenzzählung GC . Diese verwenden Objekte, um den Speicher zu verfolgen. Sie modellieren die Erreichbarkeit von Objekten mit einem gerichteten Referenzdiagramm. Wenn sich in Ihrem Objektreferenzdiagramm ein Zyklus befindet, wird der Finalizer für alle Objekte im Zyklus nie aufgerufen (bis zum Programmabschluss natürlich). Auch hier sind Finalizer-Aufrufe nicht garantiert.

TLDR

Die Müllabfuhr ist schwierig und vielfältig. Ein Finalizer-Aufruf kann vor Programmende nicht garantiert werden.


Sie haben Recht, dass dies nicht statisch oder dynamisch ist. Es ist ein Problem mit Sprachen, die überflüssig werden. Die Garbage Collection ist ein komplexes Problem und wahrscheinlich der Hauptgrund, da viele Randfälle zu berücksichtigen sind finalize(). Da Java jedoch nicht garantieren konnte, dass der Finalizer vor dem Beenden des Programms aufgerufen wird, konnte Java ihn nicht mehr unterstützen. Wenn Sie nicht sagen, dass Ihre Antwort falsch ist, ist sie möglicherweise unvollständig. Immer noch ein sehr guter Beitrag. Vielen Dank.
dbcb

Danke für die Rückmeldung. Hier ist ein Versuch, meine Antwort zu vervollständigen: Durch das explizite Weglassen von Finalisierern zwingt eine Sprache ihre Benutzer, ihre eigenen Ressourcen zu verwalten. Für viele Arten von Problemen ist das wahrscheinlich ein Nachteil. Ich persönlich bevorzuge Javas Wahl, weil ich die Macht der Finalisierer habe und nichts mich davon abhält, meinen eigenen Entsorger zu schreiben und zu benutzen. Java sagt: "Hey, Programmierer. Du bist kein Idiot, also hier ist ein Finalist. Sei einfach vorsichtig."
kdbanman

1
Meine ursprüngliche Frage wurde aktualisiert, um zu berücksichtigen, dass dies mit Sprachen zu tun hat, die vom Müll gesammelt wurden. Akzeptiere deine Antwort. Vielen Dank, dass Sie sich die Zeit genommen haben, uns zu antworten.
2.

Freue mich zu helfen. Hat meine Erläuterung der Kommentare meine Antwort klarer gemacht?
kdbanman

2
Das ist gut. Für mich ist die eigentliche Antwort hier, dass Sprachen sich dafür entscheiden, es nicht zu implementieren, da der wahrgenommene Wert die Probleme bei der Implementierung der Funktionalität nicht überwiegt. Es ist nicht unmöglich (wie Java und Python demonstrieren), aber es gibt einen Kompromiss, den viele Sprachen nicht eingehen möchten.
2.

5

In einer Nussschale

Finalisierung ist keine einfache Angelegenheit, die von Müllsammlern erledigt werden muss. Die Verwendung mit Referenzzähl-GC ist einfach. Diese GC-Familie ist jedoch häufig unvollständig, sodass Speicherlecks durch explizite Auslösung der Zerstörung und Finalisierung einiger Objekte und Strukturen ausgeglichen werden müssen. Die Verfolgung von Garbage Collectors ist wesentlich effektiver, macht es jedoch schwieriger, zu finalisierende und zu zerstörende Objekte zu identifizieren, als nur den ungenutzten Speicher zu identifizieren. Dies erfordert eine komplexere Verwaltung, kostet Zeit und Platz und ist komplexer die Umsetzung.

Einführung

Ich gehe davon aus, dass Sie nach dem Grund fragen, warum Sprachen mit Speicherbereinigung die Zerstörung / Finalisierung innerhalb des Speicherbereinigungsprozesses nicht automatisch handhaben, wie in der Bemerkung angegeben:

Ich finde es äußerst mangelhaft, dass diese Sprachen das Gedächtnis als die einzige Ressource betrachten, die es wert ist, verwaltet zu werden. Was ist mit Sockets, Dateizugriffsnummern und Anwendungsstatus?

Ich bin mit der akzeptierten Antwort von kdbanman nicht einverstanden . Obwohl die dort genannten Fakten zumeist zutreffend sind, obwohl sie stark auf die Referenzzählung abzielen, glaube ich nicht, dass sie die in der Frage beanstandete Situation richtig erklären.

Ich glaube nicht, dass die in dieser Antwort entwickelte Terminologie ein großes Problem darstellt, und es ist wahrscheinlicher, dass sie die Dinge verwirrt. In der Tat wird die Terminologie, wie dargestellt, hauptsächlich durch die Art und Weise bestimmt, wie die Prozeduren aktiviert werden, und nicht durch die Art und Weise, wie sie ausgeführt werden. Der Punkt ist, dass es in allen Fällen notwendig ist, ein Objekt, das nicht mehr benötigt wird, mit einem Bereinigungsprozess fertigzustellen und alle verwendeten Ressourcen freizugeben, wobei der Speicher nur eine davon ist. Im Idealfall sollte dies alles automatisch erfolgen, wenn das Objekt nicht mehr verwendet werden soll, und zwar mithilfe eines Müllsammlers. In der Praxis kann GC fehlen oder Mängel aufweisen. Dies wird durch die explizite Auslösung durch das Programm für Finalisierung und Rückforderung ausgeglichen.

Explizites Triggern durch das Programm ist ein Problem, da es schwer zu analysierende Programmierfehler ermöglichen kann, wenn ein noch verwendetes Objekt explizit beendet wird.

Daher ist es viel besser, sich auf die automatische Garbage Collection zu verlassen, um Ressourcen zurückzugewinnen. Es gibt jedoch zwei Probleme:

  • Einige Speicherbereinigungstechniken ermöglichen Speicherlecks, die eine vollständige Rückgewinnung von Ressourcen verhindern. Dies ist allgemein für die Referenzzählung von GC-Daten bekannt, kann jedoch für andere GC-Techniken angezeigt werden, wenn einige Datenorganisationen ohne Sorgfalt verwendet werden (hier nicht besprochener Punkt).

  • Während die GC-Technik möglicherweise die Identifizierung von nicht mehr verwendeten Speicherressourcen erleichtert, ist das Finalisieren der darin enthaltenen Objekte möglicherweise nicht einfach, und dies erschwert das Problem, andere von diesen Objekten verwendete Ressourcen zurückzugewinnen, was häufig der Zweck der Finalisierung ist.

Schließlich wird häufig vergessen, dass GC-Zyklen durch irgendetwas ausgelöst werden können, nicht nur durch Speichermangel, wenn die richtigen Hooks bereitgestellt werden und die Kosten für einen GC-Zyklus als wertvoll erachtet werden. Daher ist es vollkommen in Ordnung, eine GC einzuleiten, wenn irgendeine Art von Ressource fehlt, in der Hoffnung, eine zu befreien.

Referenzzählung Müllsammler

Die Referenzzählung ist eine schwache Müllsammeltechnik , die Zyklen nicht richtig handhabt. Es wäre in der Tat schwach, veraltete Strukturen zu zerstören und andere Ressourcen zurückzugewinnen, nur weil es schwach ist, Speicher zurückzugewinnen. Finalizer können jedoch am einfachsten mit einem Garbage Collector (GC) mit Referenzzählung verwendet werden, da ein GC mit Referenzzählung eine Struktur zurückerlangt, wenn seine Referenzzählung auf 0 abfällt. Zu diesem Zeitpunkt ist seine Adresse zusammen mit seinem Typ entweder statisch bekannt oder dynamisch. Daher ist es möglich, den Speicher genau nach dem Anwenden des richtigen Finalizers und dem rekursiven Aufrufen des Prozesses auf alle spitzen Objekte (möglicherweise über die Finalisierungsprozedur) wiederzugewinnen.

Zusammenfassend lässt sich sagen, dass die Finalisierung mit Ref Counting GC leicht zu implementieren ist, jedoch unter der "Unvollständigkeit" dieses GC leidet, und zwar aufgrund kreisförmiger Strukturen, und zwar in genau demselben Ausmaß, unter dem die Speicherfreigabe leidet. Mit anderen Worten, mit der Referenzanzahl wird der Speicher genauso schlecht verwaltet wie andere Ressourcen wie Sockets, Dateihandles usw.

Tatsächlich kann die Unfähigkeit von Ref Count GC, Schleifenstrukturen (im Allgemeinen) zurückzugewinnen, als Speicherverlust angesehen werden . Sie können nicht von allen GCs erwarten, dass sie Speicherlecks vermeiden. Dies hängt vom GC-Algorithmus und von den dynamisch verfügbaren Typstrukturinformationen ab (z. B. bei konservativem GC ).

Müllsammler aufspüren

Die leistungsstärkere Familie von GC ohne solche Lecks ist die Verfolgungsfamilie , die die aktiven Teile des Speichers ausgehend von gut identifizierten Stammzeigern untersucht. Alle Teile des Speichers, die in diesem Ablaufverfolgungsprozess nicht besucht werden (die tatsächlich auf verschiedene Arten zerlegt werden können, aber ich muss sie vereinfachen), sind nicht verwendete Teile des Speichers, die auf diese Weise zurückgewonnen werden können 1 . Diese Kollektoren rufen alle Speicherteile ab, auf die das Programm nicht mehr zugreifen kann, unabhängig davon, was es tut. Es werden kreisförmige Strukturen wiederhergestellt, und die fortgeschritteneren GC basieren auf einigen Variationen dieses Paradigmas, die manchmal hochentwickelt sind. In einigen Fällen kann es mit der Referenzzählung kombiniert werden und seine Schwächen ausgleichen.

Ein Problem ist, dass Ihre Aussage (am Ende der Frage):

Sprachen, die eine automatische Speicherbereinigung anbieten, scheinen die Hauptkandidaten für die Zerstörung / Finalisierung von Objekten zu sein, da sie mit 100% iger Sicherheit wissen, wann ein Objekt nicht mehr verwendet wird.

ist für die Rückverfolgung von Sammlern technisch falsch .

Mit 100% iger Sicherheit ist bekannt, welche Teile des Speichers nicht mehr verwendet werden . (Genauer gesagt, sie sind nicht mehr zugänglich , da einige Teile, die gemäß der Logik des Programms nicht mehr verwendet werden können, weiterhin als verwendet betrachtet werden, wenn das Programm noch einen nutzlosen Zeiger auf sie enthält Daten.) Aber weitere Verarbeitung und geeignete Strukturen sind erforderlich, um zu wissen, welche nicht verwendeten Objekte in diesen jetzt nicht verwendeten Teilen des Speichers gespeichert wurden . Dies kann aus dem, was von dem Programm bekannt ist, nicht bestimmt werden, da das Programm nicht länger mit diesen Teilen des Speichers verbunden ist.

Nach einem Durchlauf der Speicherbereinigung verbleiben also Fragmente des Speichers, die Objekte enthalten, die nicht mehr verwendet werden. Es ist jedoch a priori nicht möglich, diese Objekte zu ermitteln, um die korrekte Finalisierung anzuwenden. Wenn es sich bei dem Ablaufverfolgungskollektor um den Mark-and-Sweep-Typ handelt, können einige der Fragmente Objekte enthalten, die bereits in einem vorherigen GC-Durchgang finalisiert wurden, seitdem jedoch aus Fragmentierungsgründen nicht mehr verwendet wurden. Dies kann jedoch mit erweiterter expliziter Typisierung behoben werden.

Während ein einfacher Kollektor diese Speicherfragmente nur ohne weiteres zurückerobern würde, erfordert die Finalisierung einen bestimmten Durchgang, um den nicht verwendeten Speicher zu untersuchen, die darin enthaltenen Objekte zu identifizieren und Finalisierungsverfahren anzuwenden. Eine solche Untersuchung erfordert jedoch die Bestimmung des Objekttyps, der dort gespeichert wurde, und die Typbestimmung ist auch erforderlich, um gegebenenfalls die ordnungsgemäße Finalisierung durchzuführen.

Dies bedeutet zusätzliche Kosten in der GC-Zeit (der zusätzliche Durchlauf) und möglicherweise zusätzliche Speicherkosten, um während dieses Durchlaufs die richtigen Typinformationen durch verschiedene Techniken verfügbar zu machen. Diese Kosten können erheblich sein, da häufig nur wenige Objekte finalisiert werden sollen, während der zeitliche und räumliche Aufwand alle Objekte betreffen kann.

Ein weiterer Punkt ist, dass der zeitliche und räumliche Aufwand die Ausführung von Programmcode und nicht nur die GC-Ausführung betreffen kann.

Ich kann keine genauere Antwort geben und auf bestimmte Fragen hinweisen, da ich die Besonderheiten vieler der von Ihnen aufgelisteten Sprachen nicht kenne. Im Fall von C ist die Typisierung ein sehr schwieriges Thema, das zur Entwicklung konservativer Sammler führt. Ich vermute, dass dies auch C ++ betrifft, aber ich bin kein Experte für C ++. Dies scheint von Hans Boehm bestätigt zu werden , der einen Großteil der Forschung zur konservativen GC durchgeführt hat. Conservative GC kann nicht systematisch den gesamten nicht verwendeten Speicher zurückfordern, da möglicherweise keine genauen Typinformationen zu Daten vorliegen. Aus dem gleichen Grund wäre es nicht möglich, Finalisierungsverfahren systematisch anzuwenden.

So ist es möglich, das zu tun, wonach Sie fragen, wie Sie es aus einigen Sprachen kennen. Aber es ist nicht kostenlos. Abhängig von der Sprache und ihrer Implementierung kann dies auch dann Kosten verursachen, wenn Sie die Funktion nicht verwenden. Verschiedene Techniken und Kompromisse können in Betracht gezogen werden, um diese Probleme anzugehen, aber das würde den Rahmen einer angemessenen Antwort sprengen.

1 - Dies ist eine abstrakte Darstellung der Ablaufverfolgungssammlung (die sowohl das Kopieren als auch das Mark-and-Sweep-GC umfasst). Die Dinge variieren je nach Typ des Ablaufverfolgungssammlers, und das Erkunden des nicht verwendeten Teils des Speichers ist unterschiedlich, je nachdem, ob Kopieren oder Markieren und Sweep wird verwendet.


Sie geben viele tolle Details zur Müllabfuhr. Ihre Antwort stimmt jedoch nicht mit meiner überein - Ihre Zusammenfassung und meine TLDR sagen im Wesentlichen dasselbe. Und für das, was es wert ist, verwendet meine Antwort die Referenzzählung GC als Beispiel, keine "starke Voreingenommenheit".
kdbanman

Nachdem ich genauer gelesen habe, sehe ich die Meinungsverschiedenheit. Ich werde entsprechend bearbeiten. Auch meine Terminologie sollte eindeutig sein. Die Frage bestand darin, Finalizer und Destruktoren zusammenzubringen und sogar Disponenten im selben Atemzug zu erwähnen. Es lohnt sich, die richtigen Worte zu verbreiten.
kdbanman

@kdbanman Die Schwierigkeit bestand darin, dass ich Sie beide ansprach, da Ihre Antwort als Referenz diente. Sie können ref count nicht als paradigmatisches Beispiel verwenden, da es sich um eine schwache GC handelt, die nur selten in Sprachen verwendet wird (überprüfen Sie die vom OP angegebenen Sprachen), für die das Hinzufügen von Finalisierern eigentlich einfach wäre (aber mit eingeschränkter Verwendung). Tracingsammler werden fast immer eingesetzt. Aber Finalizer sind schwer zu fesseln, da sterbende Objekte nicht bekannt sind (entgegen der Aussage, die Sie für richtig halten). Die Unterscheidung zwischen statischer und dynamischer Typisierung ist unerheblich, da die dynamische Typisierung des Datenspeichers unerlässlich ist.
Babou

@kdbanman In Bezug auf die Terminologie ist es im Allgemeinen nützlich, da es verschiedenen Situationen entspricht. Aber hier hilft es nicht, da es um die Übergabe der Finalisierung an den GC geht. Die grundlegende GC soll nur die Zerstörung tun. Was benötigt wird, ist eine Terminologie, die unterscheidet getting memory recycled, die ich aufrufe reclamationund die vorher eine Bereinigung vornimmt, z. B. das Zurückfordern anderer Ressourcen oder das Aktualisieren einiger Objekttabellen, die ich aufrufe finalization. Dies schien mir die relevanten Themen zu sein, aber ich habe möglicherweise einen Punkt in Ihrer Terminologie übersehen, der für mich neu war.
Babou

1
Vielen Dank @ kdbanman, babou. Gute Diskussion. Ich denke, Ihre beiden Posts behandeln ähnliche Punkte. Wie Sie beide hervorheben, scheint das Kernproblem die Kategorie des Garbage Collectors zu sein, die in der Laufzeit der Sprache verwendet wird. Ich habe diesen Artikel gefunden , der einige Missverständnisse für mich ausräumt. Es scheint, dass die robusteren gcs nur Low-Level-Raw-Speicher verarbeiten, wodurch übergeordnete Objekttypen für die gc undurchsichtig werden. Ohne Kenntnis der Speicher-Interna kann der Gc keine Objekte zerstören. Welches scheint Ihre Schlussfolgerung zu sein.
Dbcb

4

Das Objektdestruktormuster ist für die Fehlerbehandlung in der Systemprogrammierung von grundlegender Bedeutung, hat jedoch nichts mit der Garbage Collection zu tun. Vielmehr hat es mit der Anpassung der Objektlebensdauer an einen Bereich zu tun und kann in jeder Sprache mit erstklassigen Funktionen implementiert / verwendet werden.

Beispiel (Pseudocode). Angenommen, Sie haben einen Rohdateityp wie den Posix-Dateideskriptortyp. Es gibt vier grundlegende Operationen open(), close(), read(), write(). Sie möchten einen "sicheren" Dateityp implementieren, der immer nach sich selbst aufräumt. (Dh das hat einen automatischen Konstruktor und Destruktor.)

Ich werde unsere Sprache hat die Ausnahmebehandlung mit übernehmen throw, tryund finally(in Sprachen ohne Ausnahmebehandlung Sie eine Disziplin einrichten können , wo der Benutzer Ihrer Art einen besonderen Wert gibt einen Fehler anzuzeigen.)

Sie richten eine Funktion ein, die eine Funktion akzeptiert, die die Arbeit erledigt. Die Worker-Funktion akzeptiert ein Argument (ein Handle auf die "sichere" Datei).

with_file_opened_for_read (string:   filename,
                           function: worker_function(safe_file f)):
  raw_file rf = open(filename, O_RDONLY)
  if rf == error:
    throw File_Open_Error

  try:
    worker_function(rf)
  finally:
    close(rf)

Sie stellen auch Implementierungen von read()und write()für safe_file(die nur das raw_file read()und aufrufen write()) bereit . Der Benutzer verwendet nun den folgenden safe_fileTyp:

...
with_file_opened_for_read ("myfile.txt",
                           anonymous_function(safe_file f):
                             mytext = read(f)
                             ... (including perhaps throwing an error)
                          )

Ein C ++ - Destruktor ist eigentlich nur syntaktischer Zucker für einen try-finallyBlock. Ich habe hier so ziemlich alles getan, was eine C ++ - safe_fileKlasse mit einem Konstruktor und einem Destruktor kompilieren würde. Beachten Sie, dass C ++ keine finallyAusnahmen hat, insbesondere weil Stroustrup der Meinung war, dass die Verwendung eines expliziten Destruktors syntaktisch besser ist (und er hat ihn in die Sprache eingeführt, bevor die Sprache anonyme Funktionen hatte).

(Dies ist eine Vereinfachung einer der Methoden, mit denen Menschen seit vielen Jahren Fehlerbehandlungen in Lisp-ähnlichen Sprachen durchführen. Ich glaube, ich bin zum ersten Mal Ende der 1980er oder Anfang der 1990er Jahre darauf gestoßen, aber ich weiß nicht mehr, wo.)


Dies beschreibt die Interna des Stack-basierten Destruktor-Musters in C ++, erklärt jedoch nicht, warum eine Garbage-Collected-Sprache eine solche Funktionalität nicht implementieren würde. Sie mögen Recht haben, dass dies nichts mit der Speicherbereinigung zu tun hat, aber es hängt mit der allgemeinen Zerstörung / Finalisierung von Objekten zusammen, die in Sprachen mit Speicherbereinigung schwierig oder ineffizient zu sein scheint. Wenn also die allgemeine Zerstörung nicht unterstützt wird, scheint die stapelbasierte Zerstörung ebenfalls wegzulassen.
dbcb

Wie ich zu Beginn sagte: Jede Sprache, die über erstklassige Funktionen (oder eine Annäherung an erstklassige Funktionen) verfügt, bietet Ihnen die Möglichkeit, "kugelsichere" Schnittstellen wie safe_fileund with_file_opened_for_read(ein Objekt, das sich selbst schließt, wenn es außerhalb des Gültigkeitsbereichs liegt) bereitzustellen ). Das ist das Wichtigste, dass es nicht die gleiche Syntax wie Konstruktoren hat, ist irrelevant. Lisp, Scheme, Java, Scala, Go, Haskell, Rust, Javascript und Clojure unterstützen alle ausreichend erstklassige Funktionen, sodass sie keine Destruktoren benötigen, um dieselbe nützliche Funktion bereitzustellen.
Wandering Logic

Ich glaube ich sehe was du sagst. Da Sprachen die grundlegenden Bausteine ​​(try / catch / finally, erstklassige Funktionen usw.) zur manuellen Implementierung destruktorähnlicher Funktionen bereitstellen, benötigen sie keine Destruktoren? Aus Gründen der Einfachheit konnte ich einige Sprachen auf diesem Weg sehen. Es ist zwar unwahrscheinlich, dass dies der Hauptgrund für alle aufgelisteten Sprachen ist, aber vielleicht ist es das, was es ist. Vielleicht bin ich nur in der großen Minderheit, die C ++ - Destruktoren liebt, und es interessiert niemanden wirklich, was sehr wohl der Grund sein könnte, warum die meisten Sprachen keine Destruktoren implementieren. Es ist ihnen einfach egal.
Dbcb

2

Dies ist keine vollständige Antwort auf die Frage, aber ich möchte ein paar Bemerkungen hinzufügen, die in den anderen Antworten oder Kommentaren nicht behandelt wurden.

  1. Die Frage geht implizit davon aus, dass es sich um eine objektorientierte Sprache im Simula-Stil handelt, die selbst einschränkend ist. In den meisten Sprachen, auch wenn es sich um Objekte handelt, ist nicht alles ein Objekt. Die Maschinerie zur Implementierung von Destruktoren würde Kosten verursachen, die nicht jeder Sprachimplementierer zu zahlen bereit ist.

  2. C ++ hat einige implizite Garantien für die Reihenfolge der Zerstörung. Wenn Sie beispielsweise eine baumartige Datenstruktur haben, werden die untergeordneten Elemente vor dem übergeordneten Element zerstört. Dies ist in GC-Sprachen nicht der Fall, sodass hierarchische Ressourcen möglicherweise in einer unvorhersehbaren Reihenfolge freigegeben werden. Dies kann für Nicht-Speicherressourcen von Bedeutung sein.


2

Bei der Entwicklung der beiden beliebtesten GC-Frameworks (Java und .NET) erwarteten die Autoren, dass die Finalisierung gut genug funktioniert, um andere Formen der Ressourcenverwaltung zu vermeiden. Viele Aspekte des Sprach- und Framework-Designs können erheblich vereinfacht werden, wenn nicht alle Funktionen für ein 100% zuverlässiges und deterministisches Ressourcenmanagement erforderlich sind. In C ++ müssen folgende Konzepte unterschieden werden:

  1. Zeiger / Referenz, der ein Objekt identifiziert, das ausschließlich dem Inhaber der Referenz gehört und das nicht durch Zeiger / Referenzen identifiziert wird, von denen der Eigentümer nichts weiß.

  2. Zeiger / Referenz, der ein gemeinsam nutzbares Objekt identifiziert, das niemandem exklusiv gehört.

  3. Zeiger / Referenz , die angibt , ein Objekt , das ist ausschließlich im Besitz vom Inhaber der Referenz, sondern , auf dem durch „Ansichten“ hat der Besitzer keine Möglichkeit , Tracking zugänglich sein kann.

  4. Zeiger / Verweis, der ein Objekt identifiziert, das eine Ansicht eines Objekts bereitstellt, dessen Eigentümer eine andere Person ist.

Wenn sich eine GC-Sprache / ein GC-Framework nicht um das Ressourcenmanagement kümmern muss, können alle oben genannten Elemente durch eine einzige Referenz ersetzt werden.

Ich würde die Idee, dass die Finalisierung die Notwendigkeit anderer Formen des Ressourcenmanagements eliminieren würde, für naiv halten, aber ob eine solche Erwartung zu diesem Zeitpunkt angemessen war oder nicht, hat die Geschichte seitdem gezeigt, dass es viele Fälle gibt, in denen ein präziseres Ressourcenmanagement erforderlich ist, als es die Finalisierung vorsieht . Ich bin zufällig der Meinung, dass die Anerkennung von Eigentumsrechten auf der Ebene der Sprache / des Frameworks ausreicht, um die Kosten zu rechtfertigen (die Komplexität muss irgendwo vorhanden sein, und die Verlagerung in die Sprache / das Framework würde den Benutzercode vereinfachen), erkenne jedoch, dass erhebliche Kosten entstehen Das Design profitiert von einer einzigen "Art" von Referenz - etwas, das nur funktioniert, wenn die Sprache / das Framework sich nicht mit Fragen der Ressourcenbereinigung befassen.


2

Warum fehlt das Paradigma des Objektdestruktors in Sprachen, die durch Müll gesammelt wurden?

Ich komme aus einem C ++ - Hintergrund, daher ist dieser Bereich für mich verwirrend.

Der Destruktor in C ++ macht eigentlich zwei Dinge zusammen. Es gibt RAM frei und es gibt Ressourcen-IDs frei.

Andere Sprachen trennen diese Bedenken, indem der GC für die Freigabe des Arbeitsspeichers zuständig ist, während eine andere Sprachfunktion die Freigabe der Ressourcen-IDs übernimmt.

Ich finde es äußerst mangelhaft, dass diese Sprachen das Gedächtnis als die einzige Ressource betrachten, die es wert ist, verwaltet zu werden.

Darum geht es bei GCs. Sie tun nur eins, um sicherzustellen, dass Ihnen nicht der Speicher ausgeht. Wenn der RAM unendlich ist, werden alle GCs ausgemustert, da es keinen wirklichen Grund mehr für ihre Existenz gibt.

Was ist mit Sockets, Dateizugriffsnummern und Anwendungsstatus?

Sprachen bieten verschiedene Möglichkeiten zum Freigeben von Ressourcen-IDs:

  • Handbuch .CloseOrDispose()über den Code verteilt

  • Handbuch .CloseOrDispose()im Handbuch " finallyBlock" verstreut

  • Handbuch „Ressourcen - ID - Blöcke“ (dh using, with, try-mit-Ressourcen , usw.) , die automatisiert .CloseOrDispose()nachdem der Block wird getan

  • garantiert „Ressource - ID - Blöcke“ , die automatisiert.CloseOrDispose() nach dem Block getan

Viele Sprachen verwenden manuelle (im Gegensatz zu garantierten) Mechanismen, die eine Möglichkeit für ein Missmanagement der Ressourcen schaffen. Nehmen Sie diesen einfachen NodeJS-Code:

require('fs').openSync('file1.txt', 'w');
// forget to .closeSync the opened file

..wenn der Programmierer vergessen hat, die geöffnete Datei zu schließen.

Solange das Programm ausgeführt wird, steckt die geöffnete Datei in der Schwebe. Dies ist einfach zu überprüfen, indem Sie versuchen, die Datei mit HxD zu öffnen und zu überprüfen, ob dies nicht möglich ist:

Bildbeschreibung hier eingeben

Das Freigeben von Ressourcen-IDs in C ++ - Destruktoren ist ebenfalls nicht garantiert. Man könnte denken , RAII funktioniert wie garantiert „Ressource - ID - Blöcke“, doch im Gegensatz zu „Ressource - ID - Blöcken“, die Sprache C ++ ist noch nicht das Objekt aus dem RAII Block bereitstellt wird geleckt , so dass der RAH - Block nie werden kann getan .


Es scheint, dass fast alle modernen Sprachen mit OOPy-Objektunterstützung wie Ruby, Javascript / ES6 / ES7, Actionscript, Lua usw. das Destruktor- / Finalisierungsparadigma vollständig weglassen. Python scheint das einzige mit seiner Klassenmethode zu sein __del__(). Warum ist das?

Weil sie Ressourcen-IDs auf andere Weise verwalten, wie oben erwähnt.

Was sind die Entscheidungen zum Sprachdesign, die dazu führen, dass diese Sprachen keine Möglichkeit haben, benutzerdefinierte Logik für die Objektentsorgung auszuführen?

Weil sie Ressourcen-IDs auf andere Weise verwalten, wie oben erwähnt.

Warum sollte eine Sprache das eingebaute Konzept von Objektinstanzen mit Klassen oder klassenähnlichen Strukturen zusammen mit benutzerdefinierten Instanzen (Konstruktoren) haben und dennoch die Zerstörungs- / Finalisierungsfunktionalität vollständig weglassen?

Weil sie Ressourcen-IDs auf andere Weise verwalten, wie oben erwähnt.

Ich konnte ein mögliches Argument dafür sehen, dass der Destruktor / Finalizer möglicherweise erst zu einem unbestimmten Zeitpunkt in der Zukunft aufgerufen wird, aber das hat Java oder Python nicht davon abgehalten, die Funktionalität zu unterstützen.

Java hat keine Destruktoren.

In den Java-Dokumenten wird Folgendes erwähnt :

Der übliche Zweck des Finalisierens besteht jedoch darin, Bereinigungsaktionen durchzuführen, bevor das Objekt unwiderruflich verworfen wird. Beispielsweise kann die Methode finalize für ein Objekt, das eine Eingabe- / Ausgabeverbindung darstellt, explizite E / A-Transaktionen ausführen, um die Verbindung zu trennen, bevor das Objekt endgültig gelöscht wird.

..aber das Einfügen von Code zur Verwaltung von Ressourcen-IDs Object.finalizerwird größtenteils als Anti-Pattern angesehen ( vgl. ). Dieser Code sollte stattdessen auf der Anrufseite geschrieben werden.

Für Personen, die das Anti-Pattern verwenden, ist ihre Rechtfertigung, dass sie möglicherweise vergessen haben , die Ressourcen-IDs an der Anrufstelle freizugeben. So tun sie es wieder in der Finalizerthread, nur für den Fall.

Was sind die Hauptgründe für das Design von Sprachen, um keine Form der Objekt-Finalisierung zu unterstützen?

Es gibt nicht viele Anwendungsfälle für Finalizer, da sie zum Ausführen von Code zwischen dem Zeitpunkt, an dem keine starken Verweise mehr auf das Objekt vorhanden sind, und dem Zeitpunkt, an dem der Speicher des Objekts vom GC zurückgefordert wird, dienen.

Ein möglicher Anwendungsfall ist, wenn Sie ein Protokoll der Zeit zwischen dem Sammeln des Objekts durch den GC und dem Zeitpunkt führen möchten, zu dem keine starken Verweise mehr auf das Objekt als solches vorhanden sind:

finalize() {
    Log(TimeNow() + ". Obj " + toString() + " is going to be memory-collected soon!"); // "soon"
}

-1

Ich habe in Dr. Dobbs wrt c ++ einen Verweis dazu gefunden, der allgemeinere Ideen hat, wonach Destruktoren in einer Sprache, in der sie implementiert sind, problematisch sind. Eine grobe Idee hier scheint zu sein, dass ein Hauptzweck von Destruktoren darin besteht, die Speicherfreigabe zu handhaben, und das ist schwer richtig zu erreichen. Der Speicher wird stückweise zugewiesen, aber verschiedene Objekte werden verbunden, und dann sind die Verantwortlichkeiten / Grenzen für die Freigabe nicht so klar.

Die Lösung für dieses Problem eines Garbage Collectors wurde vor Jahren entwickelt. Die Garbage Collection basiert jedoch nicht auf Objekten, die beim Verlassen der Methode aus dem Gültigkeitsbereich verschwinden (dies ist eine konzeptionelle Idee, die schwer zu implementieren ist), sondern auf einem Collector, der periodisch, etwas nicht deterministisch ausgeführt wird. wenn die App "Speicherdruck" (dh zu wenig Speicher) erfährt.

mit anderen Worten, das bloße menschliche Konzept eines "neu nicht genutzten Objekts" ist tatsächlich in gewisser Weise eine irreführende Abstraktion in dem Sinne, dass kein Objekt "augenblicklich" nicht genutzt werden kann. Nicht verwendete Objekte können nur "entdeckt" werden, indem ein Garbage Collection-Algorithmus ausgeführt wird, der den Objektreferenzgraphen durchläuft, und die leistungsstärksten Algorithmen werden mit Unterbrechungen ausgeführt.

Es ist möglich, dass ein besserer Garbage Collection-Algorithmus entdeckt werden muss, der unbenutzte Objekte nahezu augenblicklich identifizieren kann, was dann zu einem konsistenten Destruktor-Aufrufcode führen könnte, aber einer wurde nach vielen Jahren der Forschung in diesem Bereich nicht gefunden.

Die Lösung für Ressourcenverwaltungsbereiche wie Dateien oder Verbindungen scheint darin zu bestehen, Objekt- "Manager" zu haben, die versuchen, ihre Verwendung zu handhaben.


2
Interessante Entdeckung. Vielen Dank. Das Argument des Autors basiert darauf, dass der Destruktor zum falschen Zeitpunkt aufgerufen wird, weil Klasseninstanzen nach Wert übergeben werden, für die die Klasse keinen geeigneten Kopierkonstruktor hat (was ein echtes Problem darstellt). Dieses Szenario existiert jedoch in den meisten (wenn nicht allen) modernen dynamischen Sprachen nicht wirklich, da alles als Referenz übergeben wird, wodurch die Situation des Autors vermieden wird. Obwohl dies eine interessante Perspektive ist, glaube ich nicht, dass dies erklärt, warum die meisten Sprachen mit Speicherbereinigung die Destruktor- / Finalisierungsfunktionalität ausgelassen haben.
dbcb

2
Diese Antwort stellt den Artikel von Dr. Dobb falsch dar: Der Artikel argumentiert nicht, dass Destruktoren im Allgemeinen problematisch sind. Der Artikel argumentiert tatsächlich so: Speicherverwaltungsprimitive sind wie goto-Anweisungen, weil sie beide einfach, aber zu mächtig sind. Genauso wie goto-Anweisungen am besten in "angemessen begrenzten Kontrollstrukturen" (siehe: Dijktsra) gekapselt sind, lassen sich Speicherverwaltungsprimitive am besten in "angemessen begrenzten Datenstrukturen" kapseln. Destruktoren sind ein Schritt in diese Richtung, aber nicht weit genug. Entscheide selbst, ob das stimmt oder nicht.
kdbanman
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.