Jakie są różnice między .ToList (), .AsEnumerable (), AsQueryable ()?

182

Znam pewne różnice między LINQ a jednostkami i LINQ do obiektów, które pierwsza implementuje, IQueryablea druga implementuje, IEnumerablea zakres moich pytań mieści się w EF 5.

Moje pytanie brzmi: jaka jest różnica techniczna tych 3 metod? Widzę, że w wielu sytuacjach wszystkie działają. Widzę też użycie ich kombinacji .ToList().AsQueryable().

  1. Co dokładnie oznaczają te metody?

  2. Czy jest jakiś problem z wydajnością lub coś, co prowadziłoby do używania jednego nad drugim?

  3. Dlaczego miałoby się na przykład używać .ToList().AsQueryable()zamiast .AsQueryable()?

Amin Saqi
źródło

Odpowiedzi:

354

Jest wiele do powiedzenia na ten temat. Pozwól mi skupić się na AsEnumerablei AsQueryablei wzmianka ToList()po drodze.

Co robią te metody?

AsEnumerablei odpowiednio AsQueryableobsadzić lub przekonwertować na IEnumerablelub IQueryable. Mówię obsadę lub konwersję bez podania przyczyny:

  • Gdy obiekt źródłowy już implementuje interfejs docelowy, sam obiekt źródłowy jest zwracany, ale rzutowany na interfejs docelowy. Innymi słowy: typ nie jest zmieniany, ale typem kompilacji jest.

  • Gdy obiekt źródłowy nie implementuje interfejsu docelowego, obiekt źródłowy jest konwertowany na obiekt, który implementuje interfejs docelowy. Tak więc zarówno typ, jak i typ kompilacji są zmieniane.

Pokażę to na kilku przykładach. Mam tę małą metodę, która zgłasza typ czasu kompilacji i rzeczywisty typ obiektu ( dzięki uprzejmości Jona Skeeta ):

void ReportTypeProperties<T>(T obj)
{
    Console.WriteLine("Compile-time type: {0}", typeof(T).Name);
    Console.WriteLine("Actual type: {0}", obj.GetType().Name);
}

Spróbujmy dowolnego linq-to-sql Table<T>, który implementuje IQueryable:

ReportTypeProperties(context.Observations);
ReportTypeProperties(context.Observations.AsEnumerable());
ReportTypeProperties(context.Observations.AsQueryable());

Wynik:

Compile-time type: Table`1
Actual type: Table`1

Compile-time type: IEnumerable`1
Actual type: Table`1

Compile-time type: IQueryable`1
Actual type: Table`1

Widzisz, że sama klasa tabeli jest zawsze zwracana, ale jej reprezentacja zmienia się.

Teraz obiekt, który implementuje IEnumerable, a nie IQueryable:

var ints = new[] { 1, 2 };
ReportTypeProperties(ints);
ReportTypeProperties(ints.AsEnumerable());
ReportTypeProperties(ints.AsQueryable());

Wyniki:

Compile-time type: Int32[]
Actual type: Int32[]

Compile-time type: IEnumerable`1
Actual type: Int32[]

Compile-time type: IQueryable`1
Actual type: EnumerableQuery`1

Tu jest. AsQueryable()przekształcił tablicę w obiekt EnumerableQuery, który „reprezentuje IEnumerable<T>kolekcję jako IQueryable<T>źródło danych”. (MSDN).

Jakie jest zastosowanie?

AsEnumerablejest często używany do przełączania z dowolnej IQueryableimplementacji na LINQ na obiekty (L2O), głównie dlatego, że ta pierwsza nie obsługuje funkcji, które ma L2O. Aby uzyskać więcej informacji, zobacz Jaki jest wpływ AsEnumerable () na jednostkę LINQ? .

Na przykład w zapytaniu Entity Framework możemy użyć tylko ograniczonej liczby metod. Więc jeśli na przykład musimy użyć jednej z naszych własnych metod w zapytaniu, zwykle napisalibyśmy coś takiego

var query = context.Observations.Select(o => o.Id)
                   .AsEnumerable().Select(x => MySuperSmartMethod(x))

ToList - który zamienia an IEnumerable<T>na a List<T>- jest również często używany do tego celu. Zaletą używania AsEnumerablevs. ToListjest to, że AsEnumerablenie wykonuje zapytania. AsEnumerablezachowuje odroczone wykonanie i nie tworzy często bezużytecznej listy pośredniej.

Z drugiej strony, gdy pożądane jest wymuszone wykonanie zapytania LINQ, ToListmożna to zrobić.

AsQueryablemoże służyć do tego, aby kolekcja wymienna akceptowała wyrażenia w instrukcjach LINQ. Zobacz więcej szczegółów: Czy naprawdę potrzebuję używać AsQueryable () w kolekcji? .

Uwaga na temat nadużywania substancji!

AsEnumerabledziała jak narkotyk. Jest to szybka poprawka, ale kosztuje i nie rozwiązuje podstawowego problemu.

W wielu odpowiedziach na przepełnienie stosu widzę, że ludzie starają AsEnumerablesię naprawić prawie każdy problem z nieobsługiwanymi metodami w wyrażeniach LINQ. Ale cena nie zawsze jest jasna. Na przykład, jeśli to zrobisz:

context.MyLongWideTable // A table with many records and columns
       .Where(x => x.Type == "type")
       .Select(x => new { x.Name, x.CreateDate })

... wszystko jest starannie przetłumaczone na instrukcję SQL, która filtruje ( Where) i projekty ( Select). Oznacza to, że zarówno długość, jak i szerokość zestawu wyników SQL są odpowiednio zmniejszone.

Załóżmy teraz, że użytkownicy chcą widzieć tylko datę CreateDate. W Entity Framework szybko odkryjesz, że ...

.Select(x => new { x.Name, x.CreateDate.Date })

... nie jest obsługiwany (w momencie pisania). Ach, na szczęście jest AsEnumerablepoprawka:

context.MyLongWideTable.AsEnumerable()
       .Where(x => x.Type == "type")
       .Select(x => new { x.Name, x.CreateDate.Date })

Pewnie, pewnie działa. Ale wciąga całą tabelę do pamięci, a następnie stosuje filtr i projekcje. Cóż, większość ludzi jest wystarczająco inteligentna, aby zrobić Wherepierwsze:

context.MyLongWideTable
       .Where(x => x.Type == "type").AsEnumerable()
       .Select(x => new { x.Name, x.CreateDate.Date })

Ale nadal wszystkie kolumny są pobierane jako pierwsze, a projekcja odbywa się w pamięci.

Prawdziwa poprawka to:

context.MyLongWideTable
       .Where(x => x.Type == "type")
       .Select(x => new { x.Name, DbFunctions.TruncateTime(x.CreateDate) })

(Ale to wymaga trochę więcej wiedzy ...)

Czego NIE robią te metody?

Przywróć możliwości IQueryable

Teraz ważne zastrzeżenie. Kiedy to zrobisz

context.Observations.AsEnumerable()
                    .AsQueryable()

skończysz z obiektem źródłowym reprezentowanym jako IQueryable. (Ponieważ obie metody tylko rzutują i nie konwertują).

Ale kiedy to zrobisz

context.Observations.AsEnumerable().Select(x => x)
                    .AsQueryable()

jaki będzie wynik?

SelectProdukuje WhereSelectEnumerableIterator. Jest to wewnętrzna klasa .Net, która implementuje IEnumerable, a nieIQueryable . Tak więc nastąpiła konwersja na inny typ i kolejne AsQueryablenigdy nie mogą już zwrócić oryginalnego źródła.

Konsekwencją tego jest, że używanie AsQueryableto nie sposób magicznie wstrzyknąć dostawca zapytania z jego specyficznymi cechami w produkt przeliczalny. Załóżmy, że tak

var query = context.Observations.Select(o => o.Id)
                   .AsEnumerable().Select(x => x.ToString())
                   .AsQueryable()
                   .Where(...)

Miejsce, w którym warunek nigdy nie zostanie przetłumaczony na SQL. AsEnumerable()następnie instrukcje LINQ definitywnie przerywają połączenie z dostawcą zapytań w ramach encji.

Celowo pokazuję ten przykład, ponieważ widziałem tutaj pytania, w których ludzie na przykład próbują „wprowadzić” Includefunkcje do kolekcji, dzwoniąc AsQueryable. Kompiluje się i działa, ale nic nie robi, ponieważ obiekt bazowy nie ma Includejuż implementacji.

Wykonać

Zarówno AsQueryablei AsEnumerablenie wykonują (lub enumerate ) obiekt źródłowy. Zmieniają tylko swój typ lub reprezentację. Oba dotyczyły interfejsów IQueryablei IEnumerablesą niczym innym jak „wyliczeniem, które się czeka”. Nie są one wykonywane, dopóki nie zostaną do tego zmuszone, na przykład, jak wspomniano powyżej, przez telefon ToList().

Oznacza to, że Wykonywanie IEnumerableuzyskać dzwoniąc AsEnumerablena IQueryableobiekcie, wykona instrumentu bazowego IQueryable. Kolejne wykonanie IEnumerabletestamentu ponownie wykona IQueryable. Co może być bardzo drogie.

Konkretne implementacje

Do tej pory było to tylko o Queryable.AsQueryablei Enumerable.AsEnumerablemetod rozszerzenie. Ale oczywiście każdy może pisać metody instancji lub metody rozszerzeń o tych samych nazwach (i funkcjach).

W rzeczywistości powszechnym przykładem konkretnej AsEnumerablemetody rozszerzenia jest DataTableExtensions.AsEnumerable. DataTablenie implementuje IQueryablelub IEnumerable, więc standardowe metody rozszerzenia nie mają zastosowania.

Gert Arnold
źródło
Dzięki za odpowiedź, czy możesz podzielić się swoją odpowiedzią na trzecie pytanie OP - 3. Dlaczego warto korzystać z, na przykład .ToList (). AsQueryable () zamiast .AsQueryable ()? ?
kgzdev
@ikram Nie mogę wymyślić niczego, co byłoby przydatne. Jak wyjaśniłem, stosowanie AsQueryable()często opiera się na błędnym przekonaniu. Pozwolę sobie jednak chwilę gotować z tyłu głowy i zobaczę, czy mogę dodać więcej informacji na ten temat.
Gert Arnold
1
Świetna odpowiedź. Czy możesz sprecyzować, co się stanie, jeśli IEnumerable uzyskany przez wywołanie AsEnumerable () na IQueryable zostanie wyliczony wiele razy? Czy zapytanie zostanie wykonane wielokrotnie, czy dane już załadowane z bazy danych do pamięci zostaną ponownie wykorzystane?
antoninod
@antoninod Dobry pomysł. Gotowe.
Gert Arnold,
46

Notować()

  • Wykonaj zapytanie natychmiast

AsEnumerable ()

  • leniwy (wykonaj zapytanie później)
  • Parametr: Func<TSource, bool>
  • Załaduj KAŻDY rekord do pamięci aplikacji, a następnie obsługuj / filtruj je. (np. Where / Take / Skip, wybierze * z tabeli 1 do pamięci, a następnie wybierze pierwsze X elementów) (W tym przypadku, co zrobił: Linq-to-SQL + Linq-to-Object)

AsQueryable ()

  • leniwy (wykonaj zapytanie później)
  • Parametr: Expression<Func<TSource, bool>>
  • Konwertuj wyrażenia na T-SQL (z określonym dostawcą), zdalnie odpytuj i ładuj wyniki do pamięci aplikacji.
  • Dlatego DbSet (w Entity Framework) dziedziczy również IQueryable, aby uzyskać wydajne zapytanie.
  • Nie ładuj każdego rekordu, np. Jeśli Take (5), wygeneruje zaznaczone 5 najlepszych SQL w tle. Oznacza to, że ten typ jest bardziej przyjazny dla bazy danych SQL i dlatego zwykle ten typ ma wyższą wydajność i jest zalecany w przypadku bazy danych.
  • AsQueryable()Zwykle działa więc znacznie szybciej niż AsEnumerable()na początku generuje T-SQL, który obejmuje wszystkie warunki w Linq.
Xin
źródło
14

ToList () będzie wszystkim w pamięci i wtedy będziesz nad nim pracował. więc ToList (). gdzie (zastosuj jakiś filtr) jest wykonywany lokalnie. AsQueryable () wykona wszystko zdalnie, tzn. Filtr zostanie wysłany do bazy danych w celu zastosowania. Queryable nic nie robi, dopóki go nie wykonasz. ToList, jednak wykonuje się natychmiast.

Spójrz także na tę odpowiedź Dlaczego warto używać AsQueryable () zamiast List ()? .

EDYCJA: Również w twoim przypadku po wykonaniu ToList (), każda kolejna operacja jest lokalna, w tym AsQueryable (). Nie możesz przełączyć się na zdalne po uruchomieniu lokalnego. Mam nadzieję, że dzięki temu jest to trochę jaśniejsze.

ashutosh raina
źródło
2
„AsQueryable () wykona wszystko zdalnie” Tylko wtedy, gdy wyliczalny już jest możliwy do zapytania. W przeciwnym razie nie jest to możliwe, a wszystko nadal działa lokalnie. Pytanie ma „.... ToList (). AsQueryable ()”, które mogłyby zawierać pewne wyjaśnienia w odpowiedzi, IMO.
2

Wystąpił zły wynik na poniższym kodzie.

void DoSomething<T>(IEnumerable<T> objects){
    var single = objects.First(); //load everything into memory before .First()
    ...
}

Naprawiono za pomocą

void DoSomething<T>(IEnumerable<T> objects){
    T single;
    if (objects is IQueryable<T>)
        single = objects.AsQueryable().First(); // SELECT TOP (1) ... is used
    else
        single = objects.First();

}

W przypadku IQueryable, pozostań w IQueryable, jeśli to możliwe, staraj się nie używać jak IEnumerable.

Aktualizacja . Dzięki Gertowi Arnoldowi można to jeszcze bardziej uprościć jednym wyrażeniem .

T single =  objects is IQueryable<T> q? 
                    q.First(): 
                    objects.First();
Rm558
źródło