„Possible multiple enumeration of IEnumerable” i Lazy evaluation

Około 4 miesiące temu zainstalowałem Resharpera, który ogólnie sprawuje się świetnie i faktycznie przyczynia się do zwiększenia wydajności pracy oraz zwiększa jakość kodu. Któregoś razu żółta lampka zapaliła się przy jeden z linii, którą napisałem po czym przeczytałem „Possible multiple enumeration of IEnumerable”. Hmmm alt + enter i do przodu. Magia resharpera zadziałała, a ja zadowolony przeszedłem z tym do porządku dziennego. Ostatnio lampka znów zamigotała jednak tym razem nie wytrzymałem. Sprawdziłem… i mocno się zdziwiłem.

Rozpatrzmy następujący kod:

class Program
    {
        static void Main(string[] args)
        {
            var result = CountNumbers();
 
            Console.WriteLine("Result: {0}",result);
            Console.ReadKey();
        }
 
        public static int CountNumbers()
        {
            var numbers = GetNumbers();
 
            var even = numbers.Count(item => item % 2 == 0);
            var odd  = numbers.Count(item => item % 2 != 0);
 
            return even + odd;
        }
 
        public static IEnumerable<int> GetNumbers()
        {
            var i = 0;
 
            for (; i < 100; i++)
            {
                yield return i;
            }
                
            Console.WriteLine("Enumerations: {0}",i);
        } 
    }

Kod nie wymaga chyba dłuższego pochylenia się. Metoda GetNumbers zwraca iterator i począwszy od 0 do 99 poprzez yield return po czym wyświetla w konsoli liczbę iteracji. W Metodzie CountNumbers zmiennej numbers przypisany zostaje IEnumerable<int> po czym obliczona zostaje ilość liczb parzystych oraz nieparzystych. Suma obu wartości zostaje zwrócona i wyświetlona w konsoli. WOW, nothing special. Przeanalizujmy teraz wynik działania programu.

Screen1

Ok, wynik działania programu jest poprawny. Jednak widać wyraźnie, że coś jest nie tak. GetNumbers  pomimo jednego wywołania wyświetliło się (a zatem wykonało) 2 razy ! Czemu? Dlatego, że każda enumeracja po IEnumerable skutkuje wywołaniem metody, która ten interfejs zwróciła. Rozwiązaniem tego problemu jest zmaterializowanie IEnumerable do listy lub tablicy, czyli :

var result = CountNumbers().ToList();

Można zadać pytanie – czy to w ogóle robi mi jakąkolwiek różnicę? Jedyna słuszna odpowiedź to : TAK ! Przedstawiony wyżej przypadek nie jest ekstremalnie niewydajny, ale w przypadku gdyby GetNumbers pobierało liczby z bazy danych, każda enumeracja skutkowałaby jednym round tripem. Na koniec tej części wynik działania programu po zastosowaniu się do uwagi resharpera:

Screen2

ELEGANCKO !


Z IEnuerable związany jest jest jeszcze jeden ciekawy temat, ale najpierw zmodyfikujmy nieco poprzedni kod:

class Program
    {
        static void Main(string[] args)
        {
            GetNumbersWithConsoleWrite(); 
            Console.ReadKey();
        }
 
        public static IEnumerable<int> GetNumbersWithConsoleWrite()
        {
            var i = 0;
 
            for (; i < 100; i++)
            {
                Console.WriteLine(i);
                yield return i;
            }
        }  
    }

Jaki będzie wynik działania programu? Intuicyjnie (przynajmniej ja) powiedziałbym, że na konsoli pojawi się 100 linii, każda z kolejną wartością i, począwszy od 0Jak łatwo się domyślić jest to odpowiedź błędna ! Nic nie zostanie wyświetlone, a konsola będzie oczekiwać na wykonanie ReadKey. Dzieje się tak, ponieważ IEnumerable jest najzwyczajniej w świecie leniwe (tzw. lazy evaluation) i metoda, która ów interfejs zwraca nie zostanie wywołana do momentu, aż zajdzie taka potrzeba. W powyższym przypadku ona nie zachodzi, ponieważ wynik nie jest dalej przetwarzany. Zobaczmy zatem co stanie się kiedy zmusimy GetNumbersWithConsoleWrite do wykonania się poprzez Count:

screen3

Teraz rezultat jest zgodny z oczekiwaniami, a my możemy dalej rozkoszować się programowaniem.

You may also like...

  • andrew k

    Dzięki za artykuł, chodziło o materializację do listy za metodą GetNumbers().ToList().

    Oczywiście problem rozwiązałoby też zmaterializowanie tej listy wewnątrz metody GetNumbers() typu:
    for (int i = 0; i < 2; i++)
    {
    intList.Add(i);
    }
    return intList;
    Poprawcie mnie jeśli się mylę ale czy można skonkludować że jeśli metoda nie używa yield(nie lazy), ale operuje na fizycznej liście to nie powinno nastąpić wielokrotne wyowłanie.