W zamknięciu, co wyzwala nową instancję przechwyconej zmiennej?
Czytam książki Jona SkeetaC # w głębi.
Na stronie 156 ma przykład Listing 5.13 „Przechwytywanie wielu instancji zmiennych z wieloma delegatami”.
List<ThreadStart> list = new List<ThreadStart>();
for(int index=0; index < 5; index++;)
{
int counter = index*10;
list.Add(delegate
{
Console.WriteLine(counter);
counter++;
}
);
}
foreach(ThreadStart t in list)
{
t();
}
list[0]();
list[0]();
list[0]();
list[1]();
W wyjaśnieniu po tej liście mówi: „każda z instancji delegata przechwyciła w tym przypadku inną zmienną”.
Rozumiem to wystarczająco dobrze, ponieważ rozumiem, że za każdym razem, gdy zamykasz zmienną, kompilator generuje IL, który hermetyzuje ją w nowej klasie stworzonej specjalnie w celu umożliwienia przechwycenia tej zmiennej (zasadniczo czyniąc ją typem odniesienia, tak że wartość, do której się odnosi) nie zostanie zniszczony przez ramkę stosu aktualnie wykonanego zakresu).
Ale potem mówi o tym, co by się stało, gdybyśmy pojmaliindex
bezpośrednio zamiast tworzyćcounter
zmienna - „wszyscy delegaci mieliby tę samą zmienną”.
Tego nie rozumiem. Nie jestindex
w takim samym zakresie jakcounter
? Dlaczego kompilator również nie utworzył nowej instancjiindex
dla każdego delegata?
Uwaga: Wydaje mi się, że wymyśliłem to, pisząc to pytanie, ale zostawię to pytanie dla potomności. Myślę, że odpowiedź jest takaindex
jest w rzeczywistości w innym zakresie niżcounter
. Indeks jest zasadniczo deklarowany „poza” pętlą for ... jest to ta sama zmienna za każdym razem.
Spojrzenie na IL wygenerowane dla afor
oznacza, że zmienne są deklarowane poza pętlą (length
ii
były zmienne zadeklarowane wfor
deklaracja pętli).
.locals init (
[0] int32 length,
[1] int32 i,
[2] bool CSRozumiem to wystarczająco dobrze, ponieważ rozumiem, że za każdym razem, gdy zamykasz zmienną, kompilator generuje IL, który hermetyzuje ją w nowej klasie stworzonej specjalnie w celu umożliwienia przechwycenia tej zmiennej (zasadniczo czyniąc ją typem odniesienia, tak że wartość, do której się odnosi) nie zostanie zniszczony przez ramkę stosu aktualnie wykonanego zakresu).0000
)
IL_0000: nop
IL_0001: ldc.i4.s 10
IL_0003: stloc.0
IL_0004: ldc.i4.0
IL_0005: stloc.1
IL_0006: br.s IL_001b
// loop start (head: IL_001b)
IL_0008: nop
IL_0009: ldloca.s i
IL_000b: call instance string [mscorlib]System.Int32::ToString()
IL_0010: call void [mscorlib]System.Console::WriteLine(string)
IL_0015: nop
IL_0016: nop
IL_0017: ldloc.1
IL_0018: ldc.i4.1
IL_0019: add
IL_001a: stloc.1
IL_001b: ldloc.1
IL_001c: ldloc.0
IL_001d: clt
IL_001f: stloc.2
IL_0020: ldloc.2
IL_0021: brtrue.s IL_0008
// end loop
Jedna rzecz, którą książka mogłaby zrobić lepiej w tym temacie, to wyjaśnienie, co robi kompilator, ponieważ cała ta „magia” ma sens, jeśli zrozumiesz, że kompilator zawija zmienną zamkniętą w nowej klasie.
Popraw wszelkie nieporozumienia lub nieporozumienia, jakie mogę mieć. Ponadto możesz rozwinąć i / lub dodać do mojego wyjaśnienia.