Od czasu do czasu widzę wzmiankę o „zamknięciach” i próbowałem to sprawdzić, ale Wiki nie podaje wyjaśnienia, które rozumiem. Czy ktoś mógłby mi pomóc tutaj?
Od czasu do czasu widzę wzmiankę o „zamknięciach” i próbowałem to sprawdzić, ale Wiki nie podaje wyjaśnienia, które rozumiem. Czy ktoś mógłby mi pomóc tutaj?
Odpowiedzi:
(Zastrzeżenie: jest to podstawowe wyjaśnienie; jeśli chodzi o definicję, trochę upraszczam)
Najprostszym sposobem na myślenie o zamknięciu jest funkcja, która może być przechowywana jako zmienna (określana jako „funkcja pierwszej klasy”), która ma specjalną zdolność dostępu do innych zmiennych lokalnych w zakresie, w którym została utworzona.
Przykład (JavaScript):
var setKeyPress = function(callback) {
document.onkeypress = callback;
};
var initialize = function() {
var black = false;
document.onclick = function() {
black = !black;
document.body.style.backgroundColor = black ? "#000000" : "transparent";
}
var displayValOfBlack = function() {
alert(black);
}
setKeyPress(displayValOfBlack);
};
initialize();
Funkcje 1 przypisane do document.onclick
i displayValOfBlack
są zamknięciami. Widać, że oba odnoszą się do zmiennej boolean black
, ale ta zmienna jest przypisywana poza funkcją. Ponieważ black
jest lokalny dla zakresu, w którym funkcja została zdefiniowana , wskaźnik do tej zmiennej zostaje zachowany.
Jeśli umieścisz to na stronie HTML:
To pokazuje, że oba mają dostęp do tego samego black
i mogą być używane do przechowywania stanu bez żadnego obiektu opakowania.
Wywołanie to setKeyPress
ma pokazać, jak można przekazać funkcję tak jak każdą zmienną. Zakres zachowane w zamknięciu jest jeszcze jeden, w których funkcja została zdefiniowana.
Zamknięcia są powszechnie używane jako procedury obsługi zdarzeń, szczególnie w JavaScript i ActionScript. Dobre użycie zamknięć pomoże ci w niejawny sposób powiązać zmienne z procedurami obsługi zdarzeń bez konieczności tworzenia opakowania obiektu. Jednak nieostrożne użycie doprowadzi do wycieków pamięci (na przykład gdy nieużywana, ale zachowana procedura obsługi zdarzeń jest jedyną rzeczą, która może zatrzymać duże obiekty w pamięci, zwłaszcza obiekty DOM, zapobiegając odśmiecaniu).
1: W rzeczywistości wszystkie funkcje w JavaScript są zamknięciami.
black
zadeklarowano wewnątrz funkcji, czy to nie zostanie zniszczone, gdy stos się rozwija?
black
zadeklarowano wewnątrz funkcji, czy to nie zostanie zniszczone”. Pamiętaj również, że jeśli zadeklarujesz obiekt w funkcji, a następnie przypiszesz go do zmiennej, która żyje gdzie indziej, obiekt ten zostanie zachowany, ponieważ istnieją inne odwołania do niego.
Zamknięcie to po prostu inny sposób patrzenia na przedmiot. Obiekt to dane, do których przypisana jest jedna lub więcej funkcji. Zamknięcie to funkcja, do której przypisana jest jedna lub więcej zmiennych. Oba są w zasadzie identyczne, przynajmniej na poziomie wdrożenia. Prawdziwa różnica polega na tym, skąd pochodzą.
W programowaniu obiektowym deklarujesz klasę obiektową, definiując z góry jej zmienne składowe i metody (funkcje składowe), a następnie tworzysz instancje tej klasy. Każde wystąpienie zawiera kopię danych elementu, zainicjowaną przez konstruktora. Następnie masz zmienną typu obiektu i przekazujesz ją jako fragment danych, ponieważ skupiamy się na jej naturze jako danych.
Z drugiej strony, w zamknięciu obiekt nie jest zdefiniowany z góry jak klasa obiektu, ani też nie jest tworzony w wywołaniu konstruktora w kodzie. Zamiast tego zapisujesz zamknięcie jako funkcję w innej funkcji. Zamknięcie może odnosić się do dowolnej zmiennej lokalnej funkcji zewnętrznej, a kompilator wykrywa ją i przenosi te zmienne z obszaru stosu funkcji zewnętrznej do deklaracji ukrytego obiektu zamknięcia. Następnie masz zmienną typu zamknięcia i chociaż jest to zasadniczo obiekt pod maską, przekazujesz ją jako odniesienie do funkcji, ponieważ fokus jest skupiony na jego naturze jako funkcji.
Termin zamknięcie wynika z faktu, że fragment kodu (blok, funkcja) może mieć dowolne zmienne, które są zamykane (tj. Powiązane z wartością) przez środowisko, w którym blok kodu jest zdefiniowany.
Weźmy na przykład definicję funkcji Scala:
def addConstant(v: Int): Int = v + k
W treści funkcji znajdują się dwie nazwy (zmienne) v
i k
wskazujące dwie wartości całkowite. Nazwa v
jest powiązana, ponieważ jest zadeklarowana jako argument funkcji addConstant
(patrząc na deklarację funkcji wiemy, że v
zostanie przypisana wartość po wywołaniu funkcji). Nazwa k
jest wolna od funkcji, addConstant
ponieważ funkcja nie zawiera wskazówek co do tego, z jaką wartością k
jest związana (i jak).
Aby ocenić połączenie, takie jak:
val n = addConstant(10)
musimy przypisać k
wartość, co może się zdarzyć tylko wtedy, gdy nazwa k
jest zdefiniowana w kontekście, w którym addConstant
została zdefiniowana. Na przykład:
def increaseAll(values: List[Int]): List[Int] =
{
val k = 2
def addConstant(v: Int): Int = v + k
values.map(addConstant)
}
Teraz, gdy zdefiniowaliśmy addConstant
w kontekście, w którym k
jest zdefiniowany, addConstant
stało się zamknięciem, ponieważ wszystkie jego wolne zmienne są teraz zamknięte (powiązane z wartością): addConstant
można je wywoływać i przekazywać tak, jakby były funkcją. Zauważ, że wolna zmienna k
jest powiązana z wartością, gdy zdefiniowane jest zamknięcie , podczas gdy zmienna argumentu v
jest powiązana, gdy wywoływane jest zamknięcie .
Zamknięcie jest więc w zasadzie funkcją lub blokiem kodu, który może uzyskiwać dostęp do wartości nielokalnych za pośrednictwem swoich wolnych zmiennych po tym, jak zostaną one powiązane kontekstem.
W wielu językach, jeśli użyjesz zamknięcia tylko raz, możesz uczynić je anonimowym , np
def increaseAll(values: List[Int]): List[Int] =
{
val k = 2
values.map(v => v + k)
}
Zauważ, że funkcja bez wolnych zmiennych jest szczególnym przypadkiem zamknięcia (z pustym zestawem wolnych zmiennych). Analogicznie, anonimowa funkcja jest szczególnym przypadkiem anonimowego zamknięcia , tj. Anonimowa funkcja jest anonimowym zamknięciem bez wolnych zmiennych.
Proste wyjaśnienie w JavaScript:
var closure_example = function() {
var closure = 0;
// after first iteration the value will not be erased from the memory
// because it is bound with the returned alertValue function.
return {
alertValue : function() {
closure++;
alert(closure);
}
};
};
closure_example();
alert(closure)
użyje wcześniej utworzonej wartości closure
. Przestrzeń alertValue
nazw zwróconej funkcji zostanie połączona z przestrzenią nazw, w której closure
znajduje się zmienna. Kiedy usuniesz całą funkcję, wartość closure
zmiennej zostanie usunięta, ale do tego czasu alertValue
funkcja zawsze będzie mogła odczytać / zapisać wartość zmiennej closure
.
Jeśli uruchomisz ten kod, pierwsza iteracja przypisze wartość 0 do closure
zmiennej i przepisze funkcję, aby:
var closure_example = function(){
alertValue : function(){
closure++;
alert(closure);
}
}
A ponieważ alertValue
potrzebuje zmiennej lokalnej closure
do wykonania funkcji, wiąże się z wartością poprzednio przypisanej zmiennej lokalnej closure
.
A teraz za każdym razem, gdy wywołujesz closure_example
funkcję, wypisze ona przyrostową wartość closure
zmiennej, ponieważ alert(closure)
jest powiązana.
closure_example.alertValue()//alerts value 1
closure_example.alertValue()//alerts value 2
closure_example.alertValue()//alerts value 3
//etc.
„Zamknięcie” to w gruncie rzeczy jakiś stan lokalny i jakiś kod, połączone w pakiet. Zazwyczaj stan lokalny pochodzi z otaczającego (leksykalnego) zakresu, a kod jest (zasadniczo) funkcją wewnętrzną, która jest następnie zwracana na zewnątrz. Zamknięcie jest wówczas kombinacją przechwyconych zmiennych, które widzi funkcja wewnętrzna i kod funkcji wewnętrznej.
Jest to jedna z tych rzeczy, która jest niestety trochę trudna do wyjaśnienia z powodu braku znajomości.
Jedną z analogii, którą z powodzeniem stosowałem w przeszłości było: „wyobraź sobie, że mamy coś, co nazywamy„ książką ”, w zamknięciu pokoju,„ książka ”to ta kopia, tam w rogu, TAOCP, ale na zamknięciu stołu , to ta kopia książki Dresden Files. Więc w zależności od tego, w jakim zamknięciu jesteś, kod „daj mi książkę” powoduje różne rzeczy. ”
static
zmienną lokalną można uznać za zamknięcie? Czy zamknięcia w Haskell obejmują państwo?
static
zmienną lokalną, masz dokładnie jedną).
Trudno zdefiniować, czym jest zamknięcie bez zdefiniowania pojęcia „państwa”.
Zasadniczo w języku z pełnym zasięgiem leksykalnym, który traktuje funkcje jako wartości pierwszej klasy, dzieje się coś wyjątkowego. Gdybym miał zrobić coś takiego:
function foo(x)
return x
end
x = foo
Zmienna x
nie tylko odwołuje się, function foo()
ale także odwołuje się do stanu foo
pozostawionego podczas ostatniego powrotu. Prawdziwa magia dzieje się, gdy foo
inne funkcje są dalej zdefiniowane w jej zakresie; jest jak własne mini-środowisko (tak jak „normalnie” definiujemy funkcje w środowisku globalnym).
Funkcjonalnie może rozwiązać wiele takich samych problemów jak słowo kluczowe „static” C ++ (C?), Które zachowuje stan zmiennej lokalnej podczas wielu wywołań funkcji; jednak bardziej przypomina stosowanie tej samej zasady (zmiennej statycznej) do funkcji, ponieważ funkcje są wartościami pierwszej klasy; zamknięcie dodaje obsługę stanu całej funkcji do zapisania (nie ma nic wspólnego z funkcjami statycznymi C ++).
Traktowanie funkcji jako wartości pierwszej klasy i dodanie obsługi zamknięć oznacza również, że możesz mieć więcej niż jedną instancję tej samej funkcji w pamięci (podobnie jak klasy). Oznacza to, że możesz ponownie użyć tego samego kodu bez konieczności resetowania stanu funkcji, co jest wymagane w przypadku zmiennych statycznych C ++ wewnątrz funkcji (może się mylić?).
Oto kilka testów wsparcia zamknięcia Lua.
--Closure testing
--By Trae Barlow
--
function myclosure()
print(pvalue)--nil
local pvalue = pvalue or 10
return function()
pvalue = pvalue + 10 --20, 31, 42, 53(53 never printed)
print(pvalue)
pvalue = pvalue + 1 --21, 32, 43(pvalue state saved through multiple calls)
return pvalue
end
end
x = myclosure() --x now references anonymous function inside myclosure()
x()--nil, 20
x() --21, 31
x() --32, 42
--43, 53 -- if we iterated x() again
wyniki:
nil
20
31
42
Może być trudny i prawdopodobnie różni się w zależności od języka, ale w Lua wydaje się, że ilekroć funkcja jest wykonywana, jej stan jest resetowany. Mówię to, ponieważ wyniki z powyższego kodu byłyby inne, gdybyśmy uzyskiwali myclosure
bezpośredni dostęp do funkcji / stanu (zamiast przez anonimową funkcję, zwraca), podobnie jak w pvalue
przypadku powrotu do wartości 10; ale jeśli uzyskamy dostęp do stanu myclosure przez x (anonimowa funkcja), możesz zobaczyć, że pvalue
jest żywy i dobrze gdzieś w pamięci. Podejrzewam, że jest w tym trochę więcej, być może ktoś może lepiej wyjaśnić naturę wdrożenia.
PS: Nie znam opcji C ++ 11 (innych niż w poprzednich wersjach), więc zauważ, że to nie jest porównanie między zamknięciami w C ++ 11 i Lua. Ponadto wszystkie „linie narysowane” od Lua do C ++ są podobne do zmiennych statycznych i zamknięcia nie są w 100% takie same; nawet jeśli czasami są wykorzystywane do rozwiązywania podobnych problemów.
Nie jestem pewien, czy w powyższym przykładzie kodu funkcja anonimowa czy funkcja wyższego rzędu jest uważana za zamknięcie?
Zamknięcie to funkcja, która ma skojarzony stan:
W Perlu tworzysz takie zamknięcia:
#!/usr/bin/perl
# This function creates a closure.
sub getHelloPrint
{
# Bind state for the function we are returning.
my ($first) = @_;a
# The function returned will have access to the variable $first
return sub { my ($second) = @_; print "$first $second\n"; };
}
my $hw = getHelloPrint("Hello");
my $gw = getHelloPrint("Goodby");
&$hw("World"); // Print Hello World
&$gw("World"); // PRint Goodby World
Jeśli spojrzymy na nową funkcjonalność dostarczoną z C ++.
Pozwala również powiązać bieżący stan z obiektem:
#include <string>
#include <iostream>
#include <functional>
std::function<void(std::string const&)> getLambda(std::string const& first)
{
// Here we bind `first` to the function
// The second parameter will be passed when we call the function
return [first](std::string const& second) -> void
{ std::cout << first << " " << second << "\n";
};
}
int main(int argc, char* argv[])
{
auto hw = getLambda("Hello");
auto gw = getLambda("GoodBye");
hw("World");
gw("World");
}
Rozważmy prostą funkcję:
function f1(x) {
// ... something
}
Ta funkcja nazywa się funkcją najwyższego poziomu, ponieważ nie jest zagnieżdżona w żadnej innej funkcji. Każda funkcja JavaScript wiąże ze sobą listę obiektów zwaną „łańcuchem zakresu” . Ten łańcuch zasięgu jest uporządkowaną listą obiektów. Każdy z tych obiektów definiuje niektóre zmienne.
W funkcjach najwyższego poziomu łańcuch zasięgu składa się z jednego obiektu, obiektu globalnego. Na przykład f1
powyższa funkcja ma łańcuch zasięgu, który zawiera jeden obiekt, który definiuje wszystkie zmienne globalne. (zwróć uwagę, że termin „obiekt” tutaj nie oznacza obiektu JavaScript, to tylko obiekt zdefiniowany w implementacji, który działa jako kontener zmiennych, w którym JavaScript może „wyszukiwać” zmienne).
Po wywołaniu tej funkcji JavaScript tworzy coś, co nazywa się „Obiektem aktywacyjnym” i umieszcza go na początku łańcucha zasięgu. Ten obiekt zawiera wszystkie zmienne lokalne (na przykład x
tutaj). Stąd teraz mamy dwa obiekty w łańcuchu zasięgu: pierwszy to obiekt aktywacyjny, a pod nim obiekt globalny.
Należy bardzo uważnie zauważyć, że dwa obiekty są umieszczane w łańcuchu zakresu w RÓŻNYCH czasach. Obiekt globalny jest wstawiany, gdy funkcja jest zdefiniowana (tj. Kiedy JavaScript parsuje funkcję i tworzy obiekt funkcji), a obiekt aktywacyjny wchodzi, gdy funkcja jest wywoływana.
Teraz wiemy to:
Sytuacja staje się interesująca, gdy mamy do czynienia z funkcjami zagnieżdżonymi. Stwórzmy jeden:
function f1(x) {
function f2(y) {
// ... something
}
}
Po f1
zdefiniowaniu otrzymujemy łańcuch zasięgu zawierający tylko obiekt globalny.
Teraz, gdy f1
zostanie wywołany, łańcuch zasięgu f1
pobiera obiekt aktywacyjny. Ten obiekt aktywacyjny zawiera zmienną x
i zmienną, f2
która jest funkcją. I zauważ, że f2
się definiuje. Dlatego w tym momencie JavaScript zapisuje również nowy łańcuch zasięgu f2
. Łańcuch zakresu zapisany dla tej wewnętrznej funkcji jest obowiązującym bieżącym łańcuchem zakresu. Obecnie obowiązujący łańcuch zasięgu to łańcuch f1
. Stąd f2
jest łańcuch zasięgu f1
jest obecny łańcuch zakres - który zawiera obiekt aktywacji f1
i globalnej obiektu.
Po f2
wywołaniu otrzymuje własny obiekt aktywacyjny zawierający y
, dodany do łańcucha zasięgu, który już zawiera obiekt aktywacyjny f1
i obiekt globalny.
Gdyby zdefiniowano w nim inną funkcję zagnieżdżoną f2
, jej łańcuch zasięgu zawierałby trzy obiekty w czasie definicji (2 obiekty aktywacji dwóch funkcji zewnętrznych i obiekt globalny) oraz 4 w czasie wywołania.
Teraz rozumiemy, jak działa łańcuch zasięgu, ale nie rozmawialiśmy jeszcze o zamknięciach.
Połączenie obiektu funkcji i zakresu (zestawu powiązań zmiennych), w którym zmienne funkcji są rozwiązywane, nazywa się zamknięciem w literaturze informatycznej - JavaScript, ostateczny przewodnik Davida Flanagana
Większość funkcji jest wywoływana przy użyciu tego samego łańcucha zasięgu, który obowiązywał, gdy funkcja została zdefiniowana, i nie ma znaczenia, że w grę wchodzi zamknięcie. Zamknięcia stają się interesujące, gdy są wywoływane w innym łańcuchu zasięgu niż ten, który obowiązywał podczas ich definiowania. Dzieje się tak najczęściej, gdy zagnieżdżony obiekt funkcji jest zwracany z funkcji, w której został zdefiniowany.
Gdy funkcja powraca, ten obiekt aktywacyjny jest usuwany z łańcucha zasięgu. Jeśli nie było zagnieżdżonych funkcji, nie ma już żadnych odniesień do obiektu aktywacyjnego i pobiera on śmieci. Jeśli zdefiniowano funkcje zagnieżdżone, każda z tych funkcji ma odniesienie do łańcucha zasięgu, a ten łańcuch zasięgu odnosi się do obiektu aktywacji.
Jeśli jednak te zagnieżdżone obiekty pozostaną w obrębie swojej funkcji zewnętrznej, wówczas one same zostaną wyrzucone do pamięci wraz z obiektem aktywacji, do którego się odnoszą. Ale jeśli funkcja definiuje funkcję zagnieżdżoną i zwraca ją lub przechowuje gdzieś we właściwości, wówczas będzie istniało zewnętrzne odniesienie do funkcji zagnieżdżonej. To nie będzie śmieci, a obiekt aktywacyjny, do którego się odnosi, również nie będzie śmieci.
W naszym przykładzie, nie wracają f2
od f1
, a więc wtedy, gdy wywołanie f1
wraca, jego przedmiotem aktywacyjny będzie usunięty z sieci zakres i śmieci zebrane. Ale gdybyśmy mieli coś takiego:
function f1(x) {
function f2(y) {
// ... something
}
return f2;
}
W tym przypadku zwracany f2
będzie łańcuch zasięgu, który będzie zawierał obiekt aktywacyjny f1
, a zatem nie będzie śmieci. W tym momencie, jeśli zadzwonimy f2
, będzie mógł uzyskać dostęp do f1
zmiennej, x
nawet jeśli nas nie ma f1
.
Widzimy zatem, że funkcja utrzymuje z nią swój łańcuch zasięgu, a wraz z łańcuchem zasięgu przychodzą wszystkie obiekty aktywacyjne funkcji zewnętrznych. To jest istota zamknięcia. Mówimy, że funkcje w JavaScript mają „ zasięg leksykalny” , co oznacza, że zapisują zakres, który był aktywny, gdy był zdefiniowany, w przeciwieństwie do zakresu, który był aktywny, gdy zostały wywołane.
Istnieje wiele zaawansowanych technik programowania, które obejmują zamykanie, takie jak przybliżanie zmiennych prywatnych, programowanie sterowane zdarzeniami, częściowe zastosowanie itp.
Należy również pamiętać, że wszystko to dotyczy wszystkich języków, które obsługują zamknięcia. Na przykład PHP (5.3+), Python, Ruby itp.
Zamknięcie to optymalizacja kompilatora (inaczej cukier syntaktyczny?). Niektórzy nazywają to także Obiektem Biednego Człowieka .
Zobacz odpowiedź Erica Lipperta : (fragment poniżej)
Kompilator wygeneruje kod w następujący sposób:
private class Locals
{
public int count;
public void Anonymous()
{
this.count++;
}
}
public Action Counter()
{
Locals locals = new Locals();
locals.count = 0;
Action counter = new Action(locals.Anonymous);
return counter;
}
Ma sens?
Poprosiłeś także o porównania. Zarówno VB, jak i JScript tworzą zamknięcia w bardzo podobny sposób.