Co poniższy kod zwróci na ekranie?
var lambdas = new List<Func<int>>(); for (int index = 0; index < 5;index++ ) { lambdas.Add(() =>index); } Console.WriteLine(lambdas[0]()); Console.WriteLine(lambdas[1]()); Console.WriteLine(lambdas[2]());
Spodziewać się można 0,1,2. Jednak na ekranie ujrzymy 5,5,5. Dlaczego? Aby odpowiedzieć na te pytanie zajrzymy do Reflector’a:
private static void Main(string[] args) { List<Func<int>> lambdas; Func<int> CS$<>9__CachedAnonymousMethodDelegate1; <>c__DisplayClass2 CS$<>8__locals3; lambdas = new List<Func<int>>(); CS$<>9__CachedAnonymousMethodDelegate1 = null; CS$<>8__locals3 = new <>c__DisplayClass2(); CS$<>8__locals3.index = 0; goto Label_003C; Label_0017: if (CS$<>9__CachedAnonymousMethodDelegate1 != null) { goto Label_0028; } CS$<>9__CachedAnonymousMethodDelegate1 = new Func<int>(CS$<>8__locals3.<Main>b__0); Label_0028: lambdas.Add(CS$<>9__CachedAnonymousMethodDelegate1); CS$<>8__locals3.index += 1; Label_003C: if (CS$<>8__locals3.index < 5) { goto Label_0017; } Console.WriteLine(lambdas[0]()); Console.WriteLine(lambdas[1]()); Console.WriteLine(lambdas[2]()); return; } ));
Jak widać, tworzona jest w miejsce lambdy delegata. Aby przekazać parametry do niej, została stworzona klasa <>c__DisplayClass2() , która jest po prostu wrapperem dla parametru index. Innymi słowy, została wygenerowana delegata, która jako parametr wejściowy bierze klasę DisplayClass2, która z kolei zawiera parametr index. Wszystko byłoby w porządku gdyby instancja DisplayClass2 była tworzona wewnątrz pętli. Z kodu jasno jednak wynika, że instancja jest tworzona przed Label_0017 (jest to początek pętli). Używamy zatem tej samej instancji, a w każdej iteracji wywołujemy CS$<>8__locals3.index += 1 (locals3 to instancja obiektu DisplayClass2), co zwiększa indeks. W ostatniej iteracji osiągnie się zatem wartość 5.
Przykład pokazuje, że wyrażenia lambda są łatwe w użyciu ale jeśli nie zna się w pełni zasady działania, mogą stworzyć niespodziewane efekty. Rozwiązaniem może być przekopiowanie indeksu do pomocniczej zmiennej. C#:
var lambdas = new List<Func<int>>(); for (int index = 0; index < 5;index++ ) { int tmp = index; lambdas.Add(() => tmp); } Console.WriteLine(lambdas[0]()); Console.WriteLine(lambdas[1]()); Console.WriteLine(lambdas[2]()); }
Reflector:
List<Func<int>> lambdas; int index; <>c__DisplayClass1 CS$<>8__locals2; lambdas = new List<Func<int>>(); index = 0; goto Label_002D; Label_000A: CS$<>8__locals2 = new <>c__DisplayClass1(); CS$<>8__locals2.tmp = index; lambdas.Add(new Func<int>(CS$<>8__locals2.<Main>b__0)); index += 1; Label_002D: if (index < 5) { goto Label_000A; } Console.WriteLine(lambdas[0]()); Console.WriteLine(lambdas[1]()); Console.WriteLine(lambdas[2]()); return;
Na zakończenie dodam, że w c# 5.0 usprawniono trochę wyrażenia lambda ale o tym kiedyś indziej…
Chyba każdy kto próbował używać lambd wcześniej czy później w to wdepnął. ;]
Ja w kwestii formalnej ;). “Delegata”? A nie “delegat”. Brzmi sztucznie i nie występuje w języku polskim. Biorąc pod uwagę, że w angielskim rzeczowniki nie są w stanie oddać rodzaju, to dość dziwne tłumaczenie. W literaturze spotyka się jednak pojęcie “delegat”, co dosyć dobrze oddaje przeznaczenie tego bytu – deleguje on wykonanie czynności do innych bytów.
A tak poza tym, wartościowe spostrzeżenie. Warto zapamiętać kolejny wyjątek do pętli (znam kilka z innego języka programowania, widać, że pętla lubi je mieć w każdym języku). Nota bene rozwiązanie problemu jest w tym wypadku podobne.
Dodałbym, że całe to zjawisko związane jest z “Domknięciem”:
-http://en.wikipedia.org/wiki/Closure_%28computer_science%29
-http://stackoverflow.com/questions/595482/what-are-closures-in-c
-http://www.michalkomorowski.com/2008/12/domknicie-jak-to-dziaa.html
ReSharper takie kwiatki ładnie wyłapuje. Podkreśli tutaj index w lambdas.Add i zaproponuje bodajże “copy to local variable”