.Net: Różnice między IEnumerable i IQueryable

W przepastnych czeluściach linijek kodu, które mam przyjemność (lub obowiązek ;)) oglądać w pracy bardzo często widzę interfejs IEnumerable. Przedstawiać go zresztą dotNetowcom nie trzeba bo przecież jest znany i lubiany – jeżeli ktoś nigdy nie korzystał z pętli foreach ten, piszę to z dużą dozą pewności aczkolwiek margines błędu zawsze istnieje, nigdy nie korzystał z IEnumerable :). Są na sali tacy?

Znacznie rzadziej spotykam użycie IQueryable – czy przyczyną są tylko preferencje? Wszak i on udostępnia metodę GetEnumerator() a to właśnie ona świadczy o sile foreach. Odpowiedź jak można się domyślać jest przecząca. O samej różnicy, jeszcze do niedawna, sam nie wiele wiedziałem, po za domysłami o tym że IQueryable implementuje IEnumerable. Zacznijmy jednak, jak mawiał klasyk, od początku.

IEnumerable sam w sobie posiada tylko jedną metodę – wspomnianą już GetEnumerator, która zwraca, któż by przypuszczał implementację interfejsu IEnumerator – konstrukt definiujący sposób przeglądania kolekcji. Jak można się domyślić dokładnie to samo robi jedyna metoda IQueryable. Oprócz tego siła obydwu interfejsów leży w metodach rozszerzających takich jak Where, Select czy OrderBy. Okazuje się więc, że pytanie nie  powinno brzmieć ‘co?’ tylko ‘jak?’.

Weźmy więc najprostszy przypadek i porównajmy następujący kod, zakładając że zmienna context pozwala na dostęp do reprezentacji różnych tabel z relacyjnej bazy danych:

Na tym etapie powyższe linijki zrobią dokładnie tą samą rzecz – pobiorą wszystkie elementy tabeli Cars. W obu przypadkach zapytanie do bazy będzie miało mniej więcej taką postać:

Na razie różnic nie widać. Dołóżmy więc do naszego przykładu jakieś podzapytanie:

Tu już pojawią się różnice. Zapytanie, którego rezultat został określony jako IEnumerable, pobierze do pamięci wszystkie elementy tabeli Cars. Następnie je przefiltruje tak by zwrócić te oczekiwane przez użytkownika – wykonywane jest tutaj tzw. zapytanie lokalne. Zapytanie wykonane na kolekcji IQueryable przefiltruje Cars po stronie bazy danych, a do pamięci wczyta już przygotowane i wyselekcjonowane rekordy – tu mamy doczynienia z zapytaniem interpretowanym.

O ile pracując z kolekcjami tworzonymi i istniejącymi jedynie w pamięci nie zauważymy większej różnicy. O tyle odwołując się do kolekcji przechowywanych w bazie danych, ze względów wydajnościowych zawsze powinniśmy korzystać z IQueryable(co z resztą robi większość ORM’ów).


Na koniec warto by było odpowiedzieć na pytanie jaka magia stoi za zapytaniami interpretowanymi. Jak już wspomniałem obydwa interfejsy o których piszę pokazują swą siłę gdy używamy operatorów LINQ, czyli de facto metod rozszerzających (istnieją ich dwa zbiory po jednym dla każdego interfejsu). Wiele się wyjaśni gdy spojrzymy na ich nagłówki. Tutaj przykład dla Where:

Jak widać Where dla IEnumerable przyjmuje po prostu predykat w formie delegatu Func. IQueryable również przyjmuje predykat, ale opakowany w typ Expression<T>. Programista podczas pracy może nawet nie być świadomy tej różnicy bo wyrażenia lambda, które podaje nie różnią się niczym. Dopiero kompilator wie, że musi  postępować inaczej. W przypadku LINQ dla IEnumerable po prostu tłumaczy wyrażenie lambda na delegat, natomiast dla IQueryable tworzy coś co nazywa się drzewem wyrażenia. Drzewo wyrażenia to dane zapytania zapisane w postaci document object model, dzięki czemu może być przeglądane w czasie działania programu – co pozwala swobodnie tłumaczyć go na SQL a tym samym tworzyć wydajniejsze zapytania.

 

 

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *