Ich habe kürzlich einen Online-Kurs über Programmiersprachen besucht, in dem unter anderem Abschlussarbeiten vorgestellt wurden. Ich schreibe zwei Beispiele auf, die von diesem Kurs inspiriert wurden, um einen Kontext zu geben, bevor ich meine Frage stelle.
Das erste Beispiel ist eine SML-Funktion, die eine Liste der Zahlen von 1 bis x erzeugt, wobei x der Parameter der Funktion ist:
fun countup_from1 (x: int) =
let
fun count (from: int) =
if from = x
then from :: []
else from :: count (from + 1)
in
count 1
end
In der SML REPL:
val countup_from1 = fn : int -> int list
- countup_from1 5;
val it = [1,2,3,4,5] : int list
Die countup_from1
Funktion verwendet den Helferabschluss count
, der die Variable x
aus ihrem Kontext erfasst und verwendet .
Wenn ich im zweiten Beispiel eine Funktion aufrufe create_multiplier t
, erhalte ich eine Funktion (eigentlich einen Abschluss) zurück, die ihr Argument mit t multipliziert:
fun create_multiplier t = fn x => x * t
In der SML REPL:
- fun create_multiplier t = fn x => x * t;
val create_multiplier = fn : int -> int -> int
- val m = create_multiplier 10;
val m = fn : int -> int
- m 4;
val it = 40 : int
- m 2;
val it = 20 : int
Die Variable m
ist also an den vom Funktionsaufruf zurückgegebenen Abschluss gebunden, und jetzt kann ich sie nach Belieben verwenden.
Damit der Abschluss während seiner gesamten Lebensdauer ordnungsgemäß funktioniert, müssen wir die Lebensdauer der erfassten Variablen verlängern t
(im Beispiel ist es eine Ganzzahl, aber es kann sich um einen beliebigen Wert handeln). Soweit ich weiß, wird dies in SML durch die Garbage Collection ermöglicht: Der Verschluss behält einen Verweis auf den erfassten Wert bei, der später vom Garbage Collector entsorgt wird, wenn der Verschluss zerstört wird.
Meine Frage: Ist die Speicherbereinigung im Allgemeinen der einzig mögliche Mechanismus, um sicherzustellen, dass die Schließungen sicher sind (abrufbar während ihrer gesamten Lebensdauer)?
Oder welche anderen Mechanismen könnten die Gültigkeit von Verschlüssen ohne Garbage Collection sicherstellen: Kopieren Sie die erfassten Werte und speichern Sie sie innerhalb des Verschlusses? Die Lebensdauer des Verschlusses selbst einschränken, sodass er nach Ablauf der erfassten Variablen nicht mehr aufgerufen werden kann?
Was sind die beliebtesten Ansätze?
BEARBEITEN
Ich denke nicht, dass das obige Beispiel erklärt / implementiert werden kann, indem die erfassten Variablen in den Abschluss kopiert werden. Im Allgemeinen können die erfassten Variablen von einem beliebigen Typ sein, z. B. können sie an eine sehr große (unveränderliche) Liste gebunden sein. Daher wäre es in der Implementierung sehr ineffizient, diese Werte zu kopieren.
Der Vollständigkeit halber ist hier ein weiteres Beispiel unter Verwendung von Referenzen (und Nebenwirkungen):
(* Returns a closure containing a counter that is initialized
to 0 and is incremented by 1 each time the closure is invoked. *)
fun create_counter () =
let
(* Create a reference to an integer: allocate the integer
and let the variable c point to it. *)
val c = ref 0
in
fn () => (c := !c + 1; !c)
end
(* Create a closure that contains c and increments the value
referenced by it it each time it is called. *)
val m = create_counter ();
In der SML REPL:
val create_counter = fn : unit -> unit -> int
val m = fn : unit -> int
- m ();
val it = 1 : int
- m ();
val it = 2 : int
- m ();
val it = 3 : int
Variablen können also auch als Referenz erfasst werden und sind nach Abschluss des Funktionsaufrufs, der sie erstellt hat ( create_counter ()
), noch aktiv.
Antworten:
Interessant ist in diesem Punkt die Programmiersprache Rust.
Rust ist eine Systemsprache mit optionalem GC und wurde von Anfang an mit Closures entwickelt .
Als die anderen Variablen kommen Rostverschlüsse in verschiedenen Geschmacksrichtungen vor. Stapel Verschlüsse , die häufigsten sind für One-Shot - Nutzung. Sie leben auf dem Stapel und können sich auf alles beziehen. Eigene Closures übernehmen das Eigentum an den erfassten Variablen. Ich denke, sie leben auf dem sogenannten "Austauschhaufen", der ein globaler Haufen ist. Ihre Lebensdauer hängt davon ab, wem sie gehören. Verwaltete Abschlüsse befinden sich auf dem aufgabenlokalen Heap und werden vom GC der Aufgabe verfolgt. Ich bin mir jedoch nicht sicher, welche Einschränkungen sie bei der Erfassung haben.
quelle
Wenn Sie mit einem GC anfangen, werden Sie leider Opfer des XY-Syndroms:
Beachten Sie jedoch, dass die Idee , die Lebensdauer einer Variablen zu verlängern , für einen Abschluss nicht erforderlich ist . es ist nur vom GC überbracht worden; Die ursprüngliche Sicherheitserklärung besagt, dass nur die Variablen für geschlossene Fenster so lange gültig sein sollten wie der Verschluss (und selbst wenn dies wackelig ist, können wir sagen, dass sie bis nach dem letzten Aufruf des Verschlusses gültig sein sollten).
Es gibt im Wesentlichen zwei Ansätze , die ich sehen kann (und die möglicherweise kombiniert werden könnten):
Letzteres ist nur ein symmetrischer Ansatz. Es wird nicht oft verwendet, aber wenn Sie wie Rust ein region-aware Type-System haben, dann ist es sicherlich möglich.
quelle
Garbage Collection wird für sichere Abschlüsse beim Erfassen von Variablen nach Wert nicht benötigt. Ein prominentes Beispiel ist C ++. C ++ hat keine Standard-Garbage Collection. Lambdas in C ++ 11 sind Abschlüsse (sie erfassen lokale Variablen aus dem umgebenden Bereich). Jede von einem Lambda erfasste Variable kann angegeben werden, um nach Wert oder Referenz erfasst zu werden. Wenn es als Referenz erfasst wird, kann man sagen, dass es nicht sicher ist. Wenn eine Variable jedoch nach Wert erfasst wird, ist dies sicher, da die erfasste Kopie und die ursprüngliche Variable voneinander getrennt sind und eine unabhängige Lebensdauer haben.
In dem von Ihnen angegebenen SML-Beispiel ist es einfach zu erklären: Variablen werden nach Wert erfasst. Es ist nicht erforderlich, die Lebensdauer einer Variablen zu verlängern, da Sie ihren Wert einfach in den Abschluss kopieren können. Dies ist möglich, weil in ML keine Variablen zugewiesen werden können. Es gibt also keinen Unterschied zwischen einer Kopie und vielen unabhängigen Kopien. Obwohl SML über eine Garbage Collection verfügt, hat dies nichts mit der Erfassung von Variablen durch Closures zu tun.
Garbage Collection wird auch nicht für sichere Abschlüsse beim Erfassen von Variablen nach Referenz (Art von) benötigt. Ein Beispiel ist die Apple Blocks-Erweiterung für die Sprachen C, C ++, Objective-C und Objective-C ++. In C und C ++ gibt es keine Standard-Garbage Collection. Blockiert standardmäßig die Erfassung von Variablen nach Wert. Wenn jedoch eine lokale Variable mit deklariert wird
__block
, dann sie Blöcke erfassen scheinbar „by reference“, und sie sind sicher -. Sie auch nach dem Umfang verwendet werden können , dass der Block in wurde definiert Was hier passiert , ist , dass__block
Variablen ist eigentlich ein Spezielle Struktur darunter, und wenn Blöcke kopiert werden (Blöcke müssen kopiert werden, um sie überhaupt außerhalb des Gültigkeitsbereichs zu verwenden), verschieben sie die Struktur für die__block
Variable in den Haufen, und der Block verwaltet seinen Speicher, glaube ich durch Referenzzählung.quelle
ref
). Also, OK, man kann diskutieren, ob die Implementierung von Schließungen mit Garbage Collection zusammenhängt oder nicht, aber die obigen Aussagen sollten korrigiert werden.ref
s, Arrays usw.), die auf eine Struktur verweisen. Aber der Wert ist die Referenz selbst, nicht das, worauf er zeigt. Wenn Sievar a = ref 1
eine Kopie haben und diese erstellenvar b = a
und verwendenb
, bedeutet dies, dass Sie sie noch verwendena
? Sie haben Zugriff auf dieselbe Struktur, auf die vona
? Ja. So funktionieren diese Typen in SML und haben nichts mit Closures zu tunEine Speicherbereinigung ist nicht erforderlich, um Sperren zu implementieren. Im Jahr 2008 hat die Delphi-Sprache, bei der es sich nicht um Müll handelt, eine Implementierung von Closures hinzugefügt. Das funktioniert so:
Der Compiler erstellt ein Funktionsobjekt unter der Haube, das eine Schnittstelle implementiert, die einen Abschluss darstellt. Alle übergeordneten lokalen Variablen werden von lokalen Variablen für die einschließende Prozedur in Felder auf dem Funktionsobjekt geändert. Dies stellt sicher, dass der Zustand so lange erhalten bleibt, wie der Funktor ist.
Die Einschränkung bei diesem System besteht darin, dass alle Parameter, die als Referenz auf die einschließende Funktion übergeben werden, sowie der Ergebniswert der Funktion nicht vom Funktor erfasst werden können, da es sich nicht um Gebietsschemas handelt, deren Gültigkeitsbereich auf den der einschließenden Funktion beschränkt ist.
Auf den Funktor wird durch die Closure-Referenz verwiesen, wobei syntaktischer Zucker verwendet wird, damit er für den Entwickler wie ein Funktionszeiger anstelle eines Interfaces aussieht. Es verwendet Delphis Referenzzählsystem für Schnittstellen, um sicherzustellen, dass das Funktionsobjekt (und der gesamte Zustand, den es enthält) so lange "am Leben" bleibt, wie es benötigt, und wird dann freigegeben, wenn der Refcount auf 0 fällt.
quelle
shared_ptr
ist nicht deterministisch, da Destruktoren versuchen, auf Null zu dekrementieren.