IEnumerable – co to jest?
IEnumerable<T> to podstawowy interfejs w System.Collections.Generic, który reprezentuje sekwencję elementów, po której można pokolei przechodzić (iterować). Mówiąc prościej, kolekcja implementująca IEnumerable pozwala na pobieranie jej elementów jeden po drugim (np. w pętli foreach). Większość kolekcji w .NET (takich jak List<T>, tablica T[], List, Dictionary itp.) implementuje właśnie IEnumerable, dzięki czemu można łatwo przeglądać ich zawartość.
W kontekście LINQ, IEnumerable jest wykorzystywane głównie do pracy z danymi w pamięci (in-memory). Gdy wykonujesz zapytania LINQ na obiektach IEnumerable (tzw. LINQ to Objects), wszelkie operacje filtrowania, sortowania czy selekcji są wykonywane w kodzie C# po stronie aplikacji, iterując po elementach już znajdujących się w pamięci. Co ważne, LINQ stosuje odroczone wykonanie – oznacza to, że np. wywołanie collection.Where(predicate) nie od razu wykonuje pętlę po kolekcji, tylko zwraca nową sekwencję (IEnumerable) gotową do iteracji. Dopiero gdy zaczniesz iterować (np. pętlą lub wywołasz metodę kończącą typu .ToList() czy .Count()), następuje faktyczne przetworzenie elementów.
IQueryable – co to jest?
IQueryable<T> pochodzi z przestrzeni nazw System.Linq i dziedziczy po IEnumerable. Można o nim myśleć jako o rozszerzeniu IEnumerable o dodatkowe możliwości związane z budowaniem zapytań. Interfejs IQueryable zaprojektowano z myślą o zdalnych źródłach danych – takich jak bazy danych czy zewnętrzne usługi – i jest on implementowany przez tzw. dostawców LINQ (query providers). Przykładem jest Entity Framework: kolekcje reprezentujące tabelę bazy danych (np. DbSet<Customer>) implementują IQueryable.
Dzięki IQueryable zapytania LINQ mogą być wykonywane na poziomie źródła danych, a nie tylko w pamięci programu. Jak to działa? Otóż IQueryable nie wykonuje od razu operacji filtrujących czy selekcyjnych, lecz buduje wewnętrznie drzewo wyrażeń (expression tree) opisujące zapytanie. To drzewo wraz z informacją o dostawcy (np. dostawcy EF dla SQL) pozwala przełożyć zapytanie LINQ na konkretny język zapytań danej bazy czy źródła – najczęściej na SQL w przypadku bazy danych. Sam interfejs IQueryable udostępnia właściwość Provider (dostawca zapytań) oraz Expression (reprezentacja zapytania jako drzewo wyrażeń). Gdy ostatecznie zażądamy wyników (np. iterując lub wywołując .ToList()), to dostawca otrzymuje to drzewo wyrażeń i tłumaczy je na konkretne zapytanie do bazy danych, pobierając tylko potrzebne dane.
Kluczowe różnice między IEnumerable a IQueryable
Oba interfejsy mają podobny cel – przetwarzać sekwencje danych – ale różnią się pod kilkoma istotnymi względami. Oto najważniejsze z nich:
• Źródło danych i środowisko wykonania: IEnumerable jest zazwyczaj używane dla kolekcji już załadowanych do pamięci. Działa w kontekście aplikacji (po stronie klienta). IQueryable jest zaprojektowane do pracy z zewnętrznymi źródłami danych (np. bazą danych, usługą webową) i pozwala delegować wykonanie operacji do tego źródła (po stronie serwera). Innymi słowy, zapytania na IQueryable mogą zostać wykonane "tam, gdzie są dane", zamiast przynosić wszystkie dane lokalnie.
• Wykonanie operacji filtrujących: Główna różnica polega na tym, gdzie wykonywana jest logika zapytania (np. filtrowanie). W przypadku IEnumerable wszelkie filtry (Where, Select itd.) są wykonywane w pamięci, przez co jeśli źródłem jest baza danych, najpierw pobierasz całą kolekcję do aplikacji, a dopiero potem filtrujesz. Natomiast w przypadku IQueryable filtr jest przekładany na zapytanie do bazy i wykonywany już na serwerze bazodanowym. Tylko potrzebne wyniki trafiają do aplikacji. Można powiedzieć, że: różnica między IQueryable a IEnumerable polega na tym, gdzie wykonywana jest logika filtrowania – jedno wykonuje ją po stronie klienta, a drugie po stronie bazy danych.
• Wydajność i transfer danych: Z powyższym wiąże się wydajność. IQueryable potrafi znacząco zmniejszyć ilość danych przesyłanych przez sieć, ponieważ zwraca już przefiltrowane, zagnieżdżone lub posortowane dane prosto z bazy. To oznacza mniejszy ruch sieciowy i wykorzystanie mocy bazy danych (np. optymalizacji SQL) zamiast obciążania aplikacji. Z kolei użycie IEnumerable przy zdalnym źródle może być mniej wydajne – pobierze np. całą tabelę z bazy, po czym aplikacja dopiero przefiltruje wyniki, marnując zasoby i czas. W przypadku małych kolekcji w pamięci nie odczujesz różnicy (dane i tak są lokalnie, więc filtracja po stronie klienta jest w porządku). Jeśli dane są już w pamięci, użycie IQueryable nie ma sensu – lepiej pozostać przy IEnumerable, bo i tak wszelkie zapytania będą wykonywane lokalnie.
• Wsparcie LINQ i możliwości zapytań: Ponieważ IQueryable dziedziczy po IEnumerable, możesz na obiekcie IQueryable użyć wszystkich tych samych metod rozszerzających LINQ co na IEnumerable. Jednak pod spodem te metody działają inaczej: dla IQueryable są zdefiniowane w klasie System.Linq.Queryable i przyjmują jako parametry wyrażenia (Expression<Func<T,...>>), a nie delegaty. Dzięki temu zamiast przekazywać fragmenty kodu do wykonania, przekazujesz opis operacji (wyrażenie), które dostawca LINQ może przeanalizować i przetworzyć. Dla porównania, metody LINQ dla IEnumerable (System.Linq.Enumerable) przyjmują delegaty (np. Func<T,bool> w Where), czyli odwołania do metod, które wykonywane są bezpośrednio w C#. W praktyce oznacza to, że IQueryable pozwala na bardziej złożone operacje z udziałem bazy, podczas gdy IEnumerable ogranicza się do tego, co możesz zrobić w samej aplikacji. Pamiętaj też, że IQueryable ma narzut związany z obsługą dostawcy i tłumaczeniem zapytań – dlatego ma sens głównie tam, gdzie zyskujemy na ograniczeniu ilości danych (np. zapytania do dużych zbiorów danych w bazie). Dla małych, prostych list w pamięci narzut ten nie jest potrzebny.
• Przykład praktyczny: Wyobraź sobie bazę danych z tabelą Employees mającą 10000 rekordów. Jeśli użyjesz IEnumerable (np. wywołasz var employees = db.Employees.ToList() żeby otrzymać Listę, a następnie employees.Where(e => e.Age > 30)), to aplikacja najpierw pobierze wszystkich 10000 pracowników do pamięci, a dopiero potem odfiltruje tych powyżej 30 lat. Natomiast przy IQueryable możesz zrobić var query = db.Employees.Where(e => e.Age > 30); var results = query.ToList();. Tutaj do bazy zostanie wysłane zapytanie SELECT * FROM Employees WHERE Age > 30 – i tylko pasujące rekordy zostaną przesłane do aplikacji. Różnica w zużyciu pamięci i czasu może być ogromna, zwłaszcza przy dużych bazach danych.
Kiedy używać IEnumerable, a kiedy IQueryable?
IEnumerable sprawdza się doskonale, gdy pracujesz na danych już załadowanych do pamięci lub pochodzących z niewielkich kolekcji. Jeśli masz listę obiektów i chcesz je przefiltrować lub posortować w aplikacji – użyj IEnumerable (czyli po prostu zwykłych metod LINQ, które zwracają IEnumerable). W przypadku zapytań do bazy danych, czasem również korzysta się z IEnumerable celowo – np. gdy chcemy wykonać część operacji poza bazą. Przykładem może być sytuacja, gdy pewnej operacji nie da się wykonać w SQL (np. wywołanie specyficznej funkcji C# na danych). Wtedy możesz pobrać w miarę niewielki zestaw danych z bazy jako IEnumerable (np. poprzez .ToList()), a następnie zastosować tę operację w pamięci.
IQueryable natomiast powinieneś używać zawsze, gdy pobierasz dane z zewnętrznego źródła (baza danych, API) i możesz skorzystać z możliwości filtrowania/sortowania na serwerze. Zapytania LINQ korzystające z IQueryable zostaną przetłumaczone na natywne zapytania (np. SQL) i wykonane tam, gdzie dane się znajdują. Dzięki temu aplikacja nie przerzuca przez sieć niepotrzebnych informacji – otrzymuje tylko to, co faktycznie potrzebne. To jest domyślny tryb działania takich narzędzi jak Entity Framework czy LINQ to SQL – zwracają one IQueryables. Pamiętaj jednak, by nie zamieniać pochopnie IQueryable na IEnumerable przedwcześnie. Jeśli np. przez pomyłkę zrzutujesz zapytanie do IEnumerable lub wywołasz metodę .AsEnumerable()/.ToList() zbyt wcześnie (przed dodaniem wszystkich filtrów czy grupowań), to utracisz korzyści – zapytanie wykona się od razu i dalsze operacje będą już w pamięci. Zawsze więc staraj się stosować operacje LINQ w takiej kolejności, by maksymalnie wykorzystać możliwości bazy (np. filtruj i grupuj na IQueryable, a dopiero na końcu, gdy potrzebujesz, pobierz dane do pamięci metodą .ToList()).
Podsumowanie
Krótko mówiąc: IEnumerable i IQueryable to dwa przydatne interfejsy do pracy z kolekcjami, ale przeznaczone do różnych scenariuszy. IEnumerable działa idealnie z kolekcjami w pamięci i wykonuje operacje po stronie aplikacji (klienta). IQueryable jest stworzone do budowania zapytań wykonywanych na zewnętrznych źródłach danych – operacje takie jak filtrowanie wykonują się wtedy po stronie serwera (np. bazy danych), co często bywa znacznie bardziej wydajne. W rezultacie, używając IQueryable, ograniczasz transfer danych i wykorzystujesz możliwości np. silnika SQL do przetwarzania informacji. Z kolei IEnumerable bywa prostsze i wystarczające, gdy danych nie ma dużo lub są już u Ciebie lokalnie.
Na koniec praktyczna wskazówka: jeśli pracujesz z bazą danych w .NET (np. Entity Framework), to zwykle otrzymujesz obiekty IQueryable<T> i powinieneś jak najwięcej operacji wykonać właśnie na nich, zanim pobierzesz dane do pamięci. Natomiast jeśli masz już dane w pamięci (np. w liście), śmiało używaj LINQ na IEnumerable – wtedy kiedy dokładnie nastąpi wykonanie filtracji nie ma większego znaczenia, bo wszystko dzieje się lokalnie.
Mam nadzieję, że to wyjaśnienie rozwiało Twoje wątpliwości w temacie IEnumerable vs IQueryable. Jeśli dopiero zaczynasz swoją przygodę z C#/.NET i chcesz solidnie opanować podstawy (oraz takie koncepcje jak powyższa) w uporządkowany sposób, zachęcam Cię do sprawdzenia mojego kursu online "Zostań Programistą .NET". To kompletny program szkoleniowy, który przeprowadzi Cię od zera do pierwszej pracy jako młodszy programista C#/.NET w 3 miesiące.