2009-05-25

Opóźnione wykonanie w LINQ

Weźmy pod uwagę taki przykład:
public int Value()
{
    System.Console.WriteLine("Value()");
    return new System.Random().Next(10);
}

private void Test1()
{
    System.Console.WriteLine("step 1");
    var x2 = new[] { Value(), Value(), Value(), Value(), Value() };
    System.Console.WriteLine("step 2");
    var x3 = from v in x2 select v + Value();
    System.Console.WriteLine("step 3");
    var x4 = x3.ToList();
    System.Console.WriteLine("step 4");
    var x5 = x3.ToList();
}
Wynik działania kodu: step 1 Value() Value() Value() Value() Value() step 2 step 3 Value() Value() Value() Value() Value() step 4 Value() Value() Value() Value() Value() Widzimy, że wywoływanie funkcji Value(), z czym wiąże się ewaluacja wartości zmiennej, następuje w kroku 1, 3, 4. Nie następuje w kroku 2. Niewykonanie kodu związanego z krokiem 2 to właśnie opóźnione wykonanie, w przypadku gdyby krok 3 i 4 został pominięty wykonanie kodu w kroku 2 w ogóle by nie nastąpiło. Poza tym zauważmy, że w kroku 3 i 4 następuje następuje dwukrotne jakby wygenerowanie zmiennej 3. Czyli opóźnione wykonanie nie oznacza opóźnionej, ale jednokrotnej ewaluacji zmiennej. Opóźnione wykonanie nie jest żadną magią. Najpierw powinniśmy zrozumieć co kryje się pod zmienną typu var. Nie jest to żaden dowolny typ danych jakiego moglibyśmy się spodziewać np. w językach skryptowych. Typ zmiennej var jest jednoznacznie ustalany przez kompilator na podstawie tego, co zwraca dane wyrażenie. Jakiego typu są więc zmienne w naszym przypadku. Oto wynik dekompilacji z Reflectora:
private void Test1()
{
    Console.WriteLine("step 1");
    int[] x2 = new int[] { this.Value(), this.Value(), this.Value(), this.Value(), this.Value() };
    Console.WriteLine("step 2");
    IEnumerable<int> x3 = x2.Select<int, int>(delegate (int v) {
        return v + this.Value();
    });
    Console.WriteLine("step 3");
    List<int> x4 = x3.ToList<int>();
    Console.WriteLine("step 4");
    List<int> x5 = x3.ToList<int>();
}
I tutaj w zasadzie mamy podane wyjaśnienie dlaczego podczas kroku 2 nic się nie stało i dlaczego podczas kroku 3 i 4 zmienna x3 została dwukrotnie ewaluowana. Jest ona typu IEnumerable. I to jest cała magia opóźnionego wykonania, dopóki nie zaczniemy dokonywać enumeracji dopóki nic się nie dzieje. Większość formuł LINQ zwraca właśnie enumeratory, stąd ich wykonanie jest opóźnione. Często zdarza się, że formuły LINQ budują swoistą kaskadę i że, całe przetwarzanie jest inicjowane przez ostatnią z nich. Przykładowe operacje, które powodują enumeracje: pobranie sumy elementów, ilości elementów, wartości konkretnego elementu, konwersja do listy. Nie zawsze wszystkie elementy są enumerowane: w przypadku np. funkcji Contains() enumeracja zatrzyma się na pierwszym elemencie, który spełnia warunek, optymistycznie pierwszym, pesymistycznie po ostatnim. Kompilator nie czyni żadnych założeń co do niezmienności danych. I choć zapis formuły LINQ przypomina zapis polecenia SQL, to jednak w przeciwieństwie do SQL żadne optymalizacje związane z nieprzetwarzaniem dwa razy tego samego (np. podwójnie zagnieżdżony select) nie są w LINQ robione.
private void Test1()
{
    var x2 = new[] { Value(), Value(), Value(), Value(), Value() };
    var x3 = from v in x2 select v + Value();
    var x4 = from v in x3 select x3.Sum() + v;
    x4.ToList();
}
Podczas wykonywania x4.ToList() funkcja Value() zostanie wywołana 30 razy. Trzeba na to zwrócić uwagę. LINQ samo w sobie jest wolne, a dzięki efektom takim jak wyżej możemy je spowolnić dodatkowo. Trzeba pamiętać, że x3 nie jest zmienną posiadając jakiś stan, jest enumeratorem i każde jej użycie powoduje enumerację. Trochę może się to kłócić z naszą percepcją. Poza tym zauważmy, że ponieważ funkcja Value() zwraca wartość losową, to każde wykonanie x3.Sum() da inny wynik. Wystarczy trochę zmienić kod:
private void Test1()
{
    var x2 = new[] { Value(), Value(), Value(), Value(), Value() };
    var x3 = (from v in x2 select v + Value()).ToList();
    var x4 = from v in x3 select x3.Sum() + v;
    x4.ToList();
} 
I liczba wywołań Value() maleje o 25. Często mówi się, że wywołanie ToList() powoduje natychmiastowe wyliczenie wartości obiektu, tymczasem oznacza to, że zmienna typu var nie będzie typu IEnumerable, a typu List, co powoduje, że musi ona być wyliczona.

Brak komentarzy:

Prześlij komentarz