Spotkałem interesujący problem dotyczący C #. Mam kod jak poniżej.
List<Func<int>> actions = new List<Func<int>>();
int variable = 0;
while (variable < 5)
{
actions.Add(() => variable * 2);
++ variable;
}
foreach (var act in actions)
{
Console.WriteLine(act.Invoke());
}
Oczekuję, że wyniesie 0, 2, 4, 6, 8. Jednak faktycznie wyprowadza pięć 10.
Wydaje się, że jest to spowodowane wszystkimi działaniami odnoszącymi się do jednej uchwyconej zmiennej. W rezultacie, gdy zostaną wywołane, wszystkie mają taką samą wydajność.
Czy istnieje sposób obejścia tego limitu, aby każda instancja akcji miała własną przechwyconą zmienną?
c#
closures
captured-variable
Morgan Cheng
źródło
źródło
Captured variables are always evaluated when the delegate is actually invoked, not when the variables were captured
.Odpowiedzi:
Tak - weź kopię zmiennej wewnątrz pętli:
Możesz myśleć o tym tak, jakby kompilator C # tworzy „nową” zmienną lokalną za każdym razem, gdy trafi ona do deklaracji zmiennej. W rzeczywistości utworzy odpowiednie nowe obiekty zamknięcia i komplikuje się (pod względem implementacji), jeśli odwołujesz się do zmiennych w wielu zakresach, ale działa :)
Zauważ, że częstszym występowaniem tego problemu jest użycie
for
lubforeach
:Więcej informacji na ten temat znajduje się w sekcji 7.14.4.2 specyfikacji C # 3.0, a mój artykuł na temat zamknięć zawiera również więcej przykładów.
Zauważ, że od kompilatora C # 5 i późniejszych (nawet przy określaniu wcześniejszej wersji C #) zachowanie
foreach
zmieniło się, więc nie musisz już tworzyć kopii lokalnej. Zobacz tę odpowiedź, aby uzyskać więcej informacji.źródło
Uważam, że to, czego doświadczasz, to coś, co nazywa się Closure http://en.wikipedia.org/wiki/Closure_(computer_science) . Twoja Lamba ma odniesienie do zmiennej, której zasięg wykracza poza samą funkcję. Twoja Lamba nie jest interpretowana, dopóki jej nie wywołasz, a gdy już ją uzyska, uzyska wartość, którą ma zmienna w czasie wykonywania.
źródło
Za kulisami kompilator generuje klasę reprezentującą zamknięcie twojego wywołania metody. Używa tego pojedynczego wystąpienia klasy zamknięcia dla każdej iteracji pętli. Kod wygląda mniej więcej tak, co ułatwia zrozumienie przyczyny błędu:
To nie jest właściwie skompilowany kod z twojej próbki, ale sprawdziłem własny kod i wygląda to bardzo podobnie do tego, co faktycznie wygenerowałby kompilator.
źródło
Rozwiązaniem tego problemu jest przechowywanie potrzebnej wartości w zmiennej proxy i przechwytywanie tej zmiennej.
TO ZNACZY
źródło
To nie ma nic wspólnego z pętlami.
To zachowanie jest wyzwalane, ponieważ używasz wyrażenia lambda
() => variable * 2
w zakresie zewnętrznymvariable
nie jest zdefiniowany w wewnętrznym zakresie lambda.Wyrażenia lambda (w C # 3 +, a także anonimowe metody w C # 2) nadal tworzą rzeczywiste metody. Przekazywanie zmiennych do tych metod wiąże się z pewnymi dylematami (przekazywanie przez wartość? Przekazywanie przez referencję? C # idzie przez referencję - ale to otwiera kolejny problem, w którym referencja może przeżyć rzeczywistą zmienną). Aby rozwiązać wszystkie te dylematy, C # tworzy nową klasę pomocniczą („zamknięcie”) z polami odpowiadającymi zmiennym lokalnym używanym w wyrażeniach lambda i metodami odpowiadającymi rzeczywistym metodom lambda. Wszelkie zmiany
variable
w kodzie są faktycznie tłumaczone, aby to zmienićClosureClass.variable
Twoja pętla while aktualizuje
ClosureClass.variable
aż do osiągnięcia 10, a następnie pętle for wykonują akcje, które działają na tym samymClosureClass.variable
.Aby uzyskać oczekiwany wynik, musisz utworzyć separację między zmienną pętli a zmienną, która jest zamykana. Możesz to zrobić, wprowadzając inną zmienną, tj .:
Możesz także przenieść zamknięcie na inną metodę, aby utworzyć ten rozdział:
Możesz zaimplementować Mult jako wyrażenie lambda (niejawne zamknięcie)
lub z rzeczywistą klasą pomocnika:
W każdym razie „Zamknięcia” NIE są pojęciem związanym z pętlami , ale raczej z anonimowymi metodami / wyrażeniami lambda stosującymi zmienne o zasięgu lokalnym - chociaż niektóre nieostrożne użycie pętli wykazują pułapki zamknięcia.
źródło
Tak, musisz ustawić zakres
variable
w pętli i przekazać go w ten sposób lambdzie:źródło
Ta sama sytuacja ma miejsce w przypadku wielowątkowości (C #, .NET 4.0].
Zobacz następujący kod:
Celem jest wydrukowanie 1,2,3,4,5 w kolejności.
Wynik jest interesujący! (Może być jak 21334 ...)
Jedynym rozwiązaniem jest użycie zmiennych lokalnych.
źródło
Ponieważ nikt tutaj bezpośrednio nie cytował ECMA-334 :
W dalszej części specyfikacji
O tak, myślę, że należy wspomnieć, że w C ++ ten problem nie występuje, ponieważ możesz wybrać, czy zmienna jest przechwytywana przez wartość, czy przez referencję (patrz: Przechwytywanie lambda ).
źródło
Nazywa się to problemem zamknięcia, wystarczy użyć zmiennej kopiowania i gotowe.
źródło