Niedawno uczestniczyłem w kursie internetowym na temat języków programowania, w którym zaprezentowano między innymi zamknięcia. Zapisuję dwa przykłady zainspirowane tym kursem, aby podać kontekst, zanim zadam pytanie.
Pierwszym przykładem jest funkcja SML, która tworzy listę liczb od 1 do x, gdzie x jest parametrem funkcji:
fun countup_from1 (x: int) =
let
fun count (from: int) =
if from = x
then from :: []
else from :: count (from + 1)
in
count 1
end
W SML REPL:
val countup_from1 = fn : int -> int list
- countup_from1 5;
val it = [1,2,3,4,5] : int list
countup_from1
Funkcja wykorzystuje zamknięcie pomocnika count
, który przechwytuje i używa zmiennej x
od jej kontekstu.
W drugim przykładzie, kiedy wywołuję funkcję create_multiplier t
, zwracam funkcję (a właściwie zamknięcie), która zwielokrotnia jej argument przez t:
fun create_multiplier t = fn x => x * t
W 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
Zmienna m
jest więc powiązana z zamknięciem zwróconym przez wywołanie funkcji i teraz mogę z niej korzystać do woli.
Teraz, aby zamknięcie działało poprawnie przez cały okres jego istnienia, musimy przedłużyć czas życia przechwyconej zmiennej t
(w tym przykładzie jest ona liczbą całkowitą, ale może być wartością dowolnego typu). O ile mi wiadomo, w SML jest to możliwe dzięki wyrzucaniu elementów bezużytecznych: zamknięcie zachowuje odniesienie do przechwyconej wartości, która jest później usuwana przez moduł wyrzucający elementy bezużyteczne, gdy zamknięcie zostanie zniszczone.
Moje pytanie: czy ogólnie rzecz biorąc, czy usuwanie śmieci jest jedynym możliwym mechanizmem zapewniającym bezpieczeństwo zamknięć (możliwość wywołania przez cały okres ich użytkowania)?
Lub jakie są inne mechanizmy, które mogą zapewnić ważność zamknięć bez wyrzucania elementów bezużytecznych: Skopiuj przechwycone wartości i zapisz je w zamknięciu? Ogranicz czas życia samego zamknięcia, aby nie można go było wywoływać po wygaśnięciu przechwyconych zmiennych?
Jakie są najbardziej popularne podejścia?
EDYTOWAĆ
Nie sądzę, że powyższy przykład można wyjaśnić / wdrożyć, kopiując przechwycone zmienne (zmienne) do zamknięcia. Zasadniczo, przechwycone zmienne mogą być dowolnego typu, np. Mogą być powiązane z bardzo dużą (niezmienną) listą. Tak więc we wdrożeniu kopiowanie tych wartości byłoby bardzo nieefektywne.
Dla kompletności, oto kolejny przykład wykorzystujący odniesienia (i skutki uboczne):
(* 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 ();
W 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
Tak więc zmienne mogą być również przechwytywane przez odniesienie i nadal są żywe po zakończeniu wywołania funkcji, która je utworzyła ( create_counter ()
).
Odpowiedzi:
Język programowania Rust jest interesujący pod tym względem.
Rust jest językiem systemowym z opcjonalnym GC i od samego początku był projektowany z zamknięciami .
Podobnie jak inne zmienne, zamknięcia rdzy występują w różnych smakach. Zamknięcia stosu , najczęściej stosowane, są jednorazowe. Żyją na stosie i mogą odnosić się do wszystkiego. Posiadane zamknięcia przejmują własność przechwyconych zmiennych. Myślę, że żyją na tzw. „Stosie wymiany”, który jest globalnym stosem. Ich żywotność zależy od tego, kto jest ich właścicielem. Zarządzane zamknięcia są aktywne na stercie lokalnej zadania i są śledzone przez GC zadania. Nie jestem jednak pewien ich ograniczeń przechwytywania.
źródło
Niestety, zaczynając od GC, jesteś ofiarą zespołu XY:
Należy jednak pamiętać, niż idea przedłużające żywotność zmiennej nie jest konieczne dla zamknięcia; właśnie przyniósł GC; pierwotne oświadczenie dotyczące bezpieczeństwa mówi, że zmienne „zamknięte” powinny żyć tak długo, jak zamknięcie (a nawet to jest niepewne, moglibyśmy powiedzieć, że powinny żyć do ostatniego wywołania zamknięcia).
Zasadniczo istnieją dwa podejścia, które widzę (i można je potencjalnie połączyć):
To ostatnie jest tylko symetrycznym podejściem. Nie jest często używany, ale jeśli, podobnie jak Rust, masz system typowania dla regionu, to z pewnością jest to możliwe.
źródło
Odśmiecanie nie jest potrzebne do bezpiecznego zamykania podczas przechwytywania zmiennych według wartości. Jednym z wybitnych przykładów jest C ++. C ++ nie ma standardowego wyrzucania elementów bezużytecznych. Lambdy w C ++ 11 są zamknięciami (przechwytują zmienne lokalne z otaczającego zakresu). Każdą zmienną przechwyconą przez lambda można określić, aby była przechwytywana przez wartość lub przez odniesienie. Jeśli zostanie przechwycony przez odniesienie, możesz powiedzieć, że nie jest bezpieczny. Jeśli jednak zmienna zostanie przechwycona przez wartość, to jest bezpieczna, ponieważ przechwycona kopia i oryginalna zmienna są oddzielne i mają niezależne okresy istnienia.
W podanym przykładzie SML wyjaśnienie jest proste: zmienne są przechwytywane według wartości. Nie ma potrzeby „przedłużania żywotności” żadnej zmiennej, ponieważ można po prostu skopiować jej wartość do zamknięcia. Jest to możliwe, ponieważ w ML zmiennych nie można przypisać. Więc nie ma różnicy między jedną kopią a wieloma niezależnymi kopiami. Chociaż SML ma wyrzucanie elementów bezużytecznych, nie ma to związku z przechwytywaniem zmiennych przez zamknięcia.
Odśmiecanie nie jest również potrzebne do bezpiecznego zamykania podczas przechwytywania zmiennych przez referencję (rodzaj). Jednym z przykładów jest rozszerzenie Apple Blocks do języków C, C ++, Objective-C i Objective-C ++. W C i C ++ nie ma standardowego wyrzucania elementów bezużytecznych. Bloki domyślnie przechwytują zmienne. Jeśli jednak deklarowana jest zmienna lokalna
__block
, bloki przechwytują je pozornie „przez odniesienie” i są bezpieczne - można ich używać nawet po zakresie, w którym blok został zdefiniowany. To, co się tutaj dzieje, polega na tym, że__block
zmienne są w rzeczywistości specjalna struktura pod spodem, a kiedy bloki są kopiowane (bloki muszą zostać skopiowane, aby użyć ich poza zakresem w pierwszej kolejności), „przesuwają” strukturę__block
zmienna w stercie, a blok zarządza pamięcią, jak sądzę poprzez liczenie referencji.źródło
ref
s). Tak, OK, można dyskutować, czy wdrożenie zamknięć jest związane z odśmiecaniem, czy nie, ale powyższe stwierdzenia powinny zostać poprawione.ref
tablicach itp.), Które wskazują na strukturę. Ale wartością jest samo odniesienie, a nie rzecz, na którą wskazuje. Jeśli maszvar a = ref 1
i robisz kopięvar b = a
i używaszb
, czy to oznacza, że nadal używasza
? Masz dostęp do tej samej struktury, na którą wskazujea
? Tak. Tak właśnie działają te typy w SML i nie mają nic wspólnego z zamknięciamiOdśmiecanie nie jest konieczne do wdrożenia zamknięć. W 2008 r. Język Delphi, który nie jest śmieciami, dodał implementację zamknięć. Działa to tak:
Kompilator tworzy pod maską obiekt funktora, który implementuje interfejs reprezentujący zamknięcie. Wszystkie zamknięte zmienne lokalne zostają zmienione z lokalnych dla procedury zamykania na pola w obiekcie funktora. Zapewnia to zachowanie stanu tak długo, jak jest funktor.
Ograniczeniem tego systemu jest to, że jakikolwiek parametr przekazany przez odwołanie do funkcji zamykającej, a także wartość wyniku funkcji, nie mogą zostać przechwycone przez funktor, ponieważ nie są lokalnymi, których zakres jest ograniczony do zakresu funkcji zamykającej.
Funktor odnosi się do odwołania zamknięcia, używając cukru syntaktycznego, aby wyglądał na programistę jak wskaźnik funkcji zamiast interfejsu. Używa systemu zliczania referencji Delphi dla interfejsów, aby upewnić się, że obiekt funktora (i cały jego stan) pozostaje „żywy” tak długo, jak to konieczne, a następnie zostaje uwolniony, gdy liczba kont spadnie do zera.
źródło
shared_ptr
jest niedeterministyczne, ponieważ czynniki niszczące ścigają się do zera.