Odpowiedzi:
Mam artykuł na ten temat . (Ma wiele przykładów.)
Zasadniczo zamknięcie to blok kodu, który można wykonać w późniejszym czasie, ale który utrzymuje środowisko, w którym został utworzony po raz pierwszy - tzn. Może nadal używać zmiennych lokalnych itp. Metody, która go utworzyła, nawet po tym metoda zakończyła wykonywanie.
Ogólna funkcja zamknięć jest implementowana w języku C # za pomocą anonimowych metod i wyrażeń lambda.
Oto przykład z wykorzystaniem anonimowej metody:
using System;
class Test
{
static void Main()
{
Action action = CreateAction();
action();
action();
}
static Action CreateAction()
{
int counter = 0;
return delegate
{
// Yes, it could be done in one statement;
// but it is clearer like this.
counter++;
Console.WriteLine("counter={0}", counter);
};
}
}
Wynik:
counter=1
counter=2
Tutaj widzimy, że akcja zwrócona przez CreateAction nadal ma dostęp do zmiennej counter i może ją zwiększyć, nawet jeśli CreateAction zakończyło się.
counter
można zwiększać - kompilator generuje klasę zawierającą counter
pole, a każdy kod odnoszący się do counter
kończy się przechodzeniem przez instancję tej klasy.
Jeśli chcesz zobaczyć, jak C # implementuje zamknięcie, przeczytaj: „Znam odpowiedź (jego 42) blog”
Kompilator generuje w tle klasę do enkapsulacji metody anoymicznej i zmiennej j
[CompilerGenerated]
private sealed class <>c__DisplayClass2
{
public <>c__DisplayClass2();
public void <fillFunc>b__0()
{
Console.Write("{0} ", this.j);
}
public int j;
}
dla funkcji:
static void fillFunc(int count) {
for (int i = 0; i < count; i++)
{
int j = i;
funcArr[i] = delegate()
{
Console.Write("{0} ", j);
};
}
}
Przekształcając go w:
private static void fillFunc(int count)
{
for (int i = 0; i < count; i++)
{
Program.<>c__DisplayClass1 class1 = new Program.<>c__DisplayClass1();
class1.j = i;
Program.funcArr[i] = new Func(class1.<fillFunc>b__0);
}
}
Zamknięcia to wartości funkcjonalne, które zachowują wartości zmienne z pierwotnego zakresu. C # może używać ich w formie anonimowych delegatów.
Na bardzo prosty przykład weź ten kod C #:
delegate int testDel();
static void Main(string[] args)
{
int foo = 4;
testDel myClosure = delegate()
{
return foo;
};
int bar = myClosure();
}
Na końcu pasek zostanie ustawiony na 4, a delegat myClosure może zostać przekazany do użycia w innym miejscu programu.
Zamknięć można używać do wielu przydatnych rzeczy, takich jak opóźnione wykonanie lub w celu uproszczenia interfejsów - LINQ jest zbudowany głównie z zamknięć. Najbardziej bezpośrednim sposobem, w jaki przydaje się większość programistów, jest dodawanie procedur obsługi zdarzeń do dynamicznie tworzonych kontrolek - możesz użyć zamknięć, aby dodać zachowanie, gdy kontrola jest utworzona, zamiast przechowywać dane w innym miejscu.
Func<int, int> GetMultiplier(int a)
{
return delegate(int b) { return a * b; } ;
}
//...
var fn2 = GetMultiplier(2);
var fn3 = GetMultiplier(3);
Console.WriteLine(fn2(2)); //outputs 4
Console.WriteLine(fn2(3)); //outputs 6
Console.WriteLine(fn3(2)); //outputs 6
Console.WriteLine(fn3(3)); //outputs 9
Zamknięcie to anonimowa funkcja przekazana poza funkcję, w której została utworzona. Zachowuje wszelkie zmienne z funkcji, w której zostało utworzone, z której korzysta.
Oto wymyślony przykład dla C #, który utworzyłem z podobnego kodu w JavaScript:
public delegate T Iterator<T>() where T : class;
public Iterator<T> CreateIterator<T>(IList<T> x) where T : class
{
var i = 0;
return delegate { return (i < x.Count) ? x[i++] : null; };
}
Oto kod, który pokazuje, jak użyć powyższego kodu ...
var iterator = CreateIterator(new string[3] { "Foo", "Bar", "Baz"});
// So, although CreateIterator() has been called and returned, the variable
// "i" within CreateIterator() will live on because of a closure created
// within that method, so that every time the anonymous delegate returned
// from it is called (by calling iterator()) it's value will increment.
string currentString;
currentString = iterator(); // currentString is now "Foo"
currentString = iterator(); // currentString is now "Bar"
currentString = iterator(); // currentString is now "Baz"
currentString = iterator(); // currentString is now null
Mam nadzieję, że jest to trochę pomocne.
Zasadniczo zamknięcie to blok kodu, który można przekazać jako argument funkcji. C # obsługuje zamknięcia w formie anonimowych delegatów.
Oto prosty przykład:
Metoda List.Find może zaakceptować i wykonać fragment kodu (zamknięcie) w celu znalezienia elementu listy.
// Passing a block of code as a function argument
List<int> ints = new List<int> {1, 2, 3};
ints.Find(delegate(int value) { return value == 1; });
Używając składni C # 3.0 możemy zapisać to jako:
ints.Find(value => value == 1);
Zamknięcie ma miejsce, gdy funkcja jest zdefiniowana w innej funkcji (lub metodzie) i wykorzystuje zmienne z metody nadrzędnej . Takie użycie zmiennych, które są zlokalizowane w metodzie i opakowane w zdefiniowaną w niej funkcję, nazywa się zamknięciem.
Mark Seemann ma kilka interesujących przykładów zamknięć w swoim poście na blogu, w którym działa równolegle między programowaniem a programowaniem funkcjonalnym.
I żeby było bardziej szczegółowe
var workingDirectory = new DirectoryInfo(Environment.CurrentDirectory);//when this variable
Func<int, string> read = id =>
{
var path = Path.Combine(workingDirectory.FullName, id + ".txt");//is used inside this function
return File.ReadAllText(path);
};//the entire process is called a closure.
Zamknięcia to fragmenty kodu, które odwołują się do zmiennej poza sobą (od dołu na stosie), które mogą zostać wywołane lub wykonane później (na przykład, gdy zdefiniowane jest zdarzenie lub delegat, i mogą zostać wywołane w nieokreślonym czasie w przyszłości ) ... Ponieważ zewnętrzna zmienna, do której fragment odwołania do kodu mógł wyjść poza zakres (i w przeciwnym razie zostałby utracony), fakt, że odwołuje się do niego fragment kodu (zwany zamknięciem), mówi środowisku wykonawczemu, aby „wstrzymał” „ta zmienna ma zasięg, dopóki nie będzie już potrzebna przez fragment kodu zamknięcia ...
Próbowałem to też zrozumieć, poniżej znajdują się fragmenty kodu dla tego samego kodu w JavaScript i C # pokazujące zamknięcie.
JavaScript:
var c = function ()
{
var d = 0;
function inner() {
d++;
alert(d);
}
return inner;
};
var a = c();
var b = c();
<body>
<input type=button value=call onClick="a()"/>
<input type=button value=call onClick="b()"/>
</body>
DO#:
using System.IO;
using System;
class Program
{
static void Main()
{
var a = new a();
var b = new a();
a.call();
a.call();
a.call();
b.call();
b.call();
b.call();
}
}
public class a {
int b = 0;
public void call()
{
b++;
Console.WriteLine(b);
}
}
JavaScript:
var c = function ()
{
var d = 0;
function inner() {
d++;
alert(d);
}
return inner;
};
var a = c();
<input type=button value=call onClick="a()"/>
<input type=button value=call onClick="a()"/>
DO#:
using System.IO;
using System;
class Program
{
static void Main()
{
var a = new a();
var b = new a();
a.call();
a.call();
a.call();
b.call();
b.call();
b.call();
}
}
public class a {
static int b = 0;
public void call()
{
b++;
Console.WriteLine(b);
}
}
Prosta i bardziej zrozumiała odpowiedź z książki w skrócie C # 7.0.
Wymagania wstępne, które powinieneś wiedzieć : Wyrażenie lambda może odwoływać się do zmiennych lokalnych i parametrów metody, w której jest zdefiniowane (zmienne zewnętrzne).
static void Main()
{
int factor = 2;
//Here factor is the variable that takes part in lambda expression.
Func<int, int> multiplier = n => n * factor;
Console.WriteLine (multiplier (3)); // 6
}
Część rzeczywista : Zmienne zewnętrzne, do których odwołuje się wyrażenie lambda, nazywane są zmiennymi przechwyconymi. Wyrażenie lambda, które przechwytuje zmienne, nazywa się zamknięciem.
Ostatni punkt, na który należy zwrócić uwagę : Przechwycone zmienne są oceniane podczas rzeczywistego wywołania delegata, a nie podczas przechwytywania zmiennych
int factor = 2;
Func<int, int> multiplier = n => n * factor;
factor = 10;
Console.WriteLine (multiplier (3)); // 30
Jeśli napiszesz wbudowaną anonimową metodę (C # 2) lub (najlepiej) wyrażenie Lambda (C # 3 +), nadal tworzona jest faktyczna metoda. Jeśli ten kod używa zmiennej lokalnej o zasięgu zewnętrznym - nadal musisz jakoś przekazać tę zmienną do metody.
np. weź tę klauzulę Linq Where (która jest prostą metodą rozszerzenia, która przekazuje wyrażenie lambda):
var i = 0;
var items = new List<string>
{
"Hello","World"
};
var filtered = items.Where(x =>
// this is a predicate, i.e. a Func<T, bool> written as a lambda expression
// which is still a method actually being created for you in compile time
{
i++;
return true;
});
jeśli chcesz użyć i w tym wyrażeniu lambda, musisz przekazać ją do utworzonej metody.
Pierwsze pytanie, które się pojawia, brzmi: czy powinno być przekazywane przez wartość czy odniesienie?
Przekazywanie przez referencję jest (jak sądzę) bardziej korzystne, gdy uzyskujesz dostęp do odczytu / zapisu do tej zmiennej (i to właśnie robi C #; Myślę, że zespół w Microsoft rozważał zalety i wady i poszedł z odniesieniem; Według Jona Skeeta artykuł , Java poszedł z wartościami).
Ale pojawia się kolejne pytanie: gdzie to przydzielić?
Czy powinien / powinien być faktycznie / naturalnie przydzielony na stosie? Cóż, jeśli przydzielisz go na stos i przekażesz przez referencję, mogą wystąpić sytuacje, w których przeżyje własną ramkę stosu. Weź ten przykład:
static void Main(string[] args)
{
Outlive();
var list = whereItems.ToList();
Console.ReadLine();
}
static IEnumerable<string> whereItems;
static void Outlive()
{
var i = 0;
var items = new List<string>
{
"Hello","World"
};
whereItems = items.Where(x =>
{
i++;
Console.WriteLine(i);
return true;
});
}
Wyrażenie lambda (w klauzuli Where) ponownie tworzy metodę, która odwołuje się do i. Jeśli i zostanie przydzielone na stosie Outlive, to do czasu wyliczenia whereItems, i użyte w wygenerowanej metodzie wskaże i na Outlive, tj. Miejsce na stosie, które nie jest już dostępne.
Ok, więc potrzebujemy go na stosie.
Więc to, co kompilator C # robi w celu obsługi tego wbudowanego anonimowego / lambda, polega na użyciu tak zwanych „ zamknięć ”: Tworzy klasę na stosie o nazwie ( raczej słabo ) DisplayClass, która zawiera pole zawierające i oraz funkcję, która faktycznie używa to.
Coś, co byłoby równoważne z tym (możesz zobaczyć IL wygenerowaną za pomocą ILSpy lub ILDASM):
class <>c_DisplayClass1
{
public int i;
public bool <GetFunc>b__0()
{
this.i++;
Console.WriteLine(i);
return true;
}
}
Tworzy instancję tej klasy w twoim zasięgu lokalnym i zastępuje każdy kod związany z wyrażeniem i lub lambda tą instancją zamknięcia. Tak więc - za każdym razem, gdy używasz „i” w kodzie „zasięgu lokalnego”, w którym został zdefiniowany, faktycznie używasz tego pola instancji DisplayClass.
Więc jeśli zmienię „lokalne” i w metodzie głównej, faktycznie zmieni ona _DisplayClass.i;
to znaczy
var i = 0;
var items = new List<string>
{
"Hello","World"
};
var filtered = items.Where(x =>
{
i++;
return true;
});
filtered.ToList(); // will enumerate filtered, i = 2
i = 10; // i will be overwriten with 10
filtered.ToList(); // will enumerate filtered again, i = 12
Console.WriteLine(i); // should print out 12
wypisze 12, ponieważ „i = 10” trafia do tego pola disalyclass i zmienia je tuż przed 2. wyliczeniem.
Dobrym źródłem na ten temat jest ten moduł Bart De Smet Pluralsight (wymaga rejestracji) (również zignoruj jego błędne użycie terminu „Podnoszenie” - co (myślę) ma na myśli to, że zmienna lokalna (tj. I) została zmieniona tak, aby odwoływała się do nowego pola DisplayClass).
W innych wiadomościach wydaje się, że istnieje pewne błędne przekonanie, że „Zamknięcia” są powiązane z pętlami - jak rozumiem, „Zamknięcia” NIE są pojęciem związanym z pętlami , ale raczej z użyciem anonimowych metod / wyrażeń lambda z wykorzystaniem zmiennych lokalnych o zasięgu - chociaż pewne sztuczki pytania używają pętli, aby to wykazać.
Zamknięcie to funkcja zdefiniowana w funkcji, która może uzyskiwać dostęp do lokalnych zmiennych zarówno jej, jak i jej elementu nadrzędnego.
public string GetByName(string name)
{
List<things> theThings = new List<things>();
return theThings.Find<things>(t => t.Name == name)[0];
}
więc funkcja wewnątrz metody find.
t => t.Name == name
może uzyskać dostęp do zmiennych w swoim zakresie, t oraz nazwie zmiennej, która jest w zakresie nadrzędnym. Mimo że jest wykonywany metodą find jako delegat, z innego zakresu razem.