Wyrażenia lambda i niespodziewany rezultat

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…

5 thoughts on “Wyrażenia lambda i niespodziewany rezultat”

  1. 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.

  2. 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

  3. ReSharper takie kwiatki ładnie wyłapuje. Podkreśli tutaj index w lambdas.Add i zaproponuje bodajże “copy to local variable”

Leave a Reply

Your email address will not be published.