Jeśli napiszę coś takiego:
var things = mythings
.Where(x => x.IsSomeValue)
.Where(y => y.IsSomeOtherValue)
Czy to to samo, co:
var results1 = new List<Thing>();
foreach(var t in mythings)
if(t.IsSomeValue)
results1.Add(t);
var results2 = new List<Thing>();
foreach(var t in results1)
if(t.IsSomeOtherValue)
results2.Add(t);
A może pod przykryciem jest jakaś magia, która działa mniej więcej tak:
var results = new List<Thing>();
foreach(var t in mythings)
if(t.IsSomeValue && t.IsSomeOtherValue)
results.Add(t);
A może jest to coś zupełnie innego?
Odpowiedzi:
Zapytania LINQ są leniwe . To oznacza kod:
robi bardzo mało. Pierwotny enumerable (
mythings
) jest wyliczany tylko wtedy, gdy wynikowy enumerable (things
) jest zużywany, np. Przezforeach
pętlę.ToList()
lub.ToArray()
.Jeśli zadzwonisz
things.ToList()
, jest to mniej więcej odpowiednik twojego ostatniego kodu, być może z pewnym (zwykle nieistotnym) narzutem z liczników.Podobnie, jeśli używasz pętli foreach:
Jego działanie jest podobne do:
Niektóre zalety wydajności lenistwa dla wyliczeń (w przeciwieństwie do obliczania wszystkich wyników i przechowywania ich na liście) polegają na tym, że zużywa bardzo mało pamięci (ponieważ przechowywany jest tylko jeden wynik na raz) i że nie ma znaczącego wzrostu koszt początkowy.
Jeśli wyliczalny jest tylko częściowo wyliczony, jest to szczególnie ważne. Rozważ ten kod:
Sposób, w jaki LINQ jest implementowany,
mythings
będzie wyliczany tylko do pierwszego elementu, który pasuje do twoich warunków where. Jeśli ten element znajduje się na początku listy, może to być ogromny wzrost wydajności (np. O (1) zamiast O (n)).źródło
foreach
jest to, że LINQ używa wywołań delegowanych, które mają pewne obciążenie. Może to być znaczące, gdy warunki działają bardzo szybko (co często robią).ToList
lubToArray
. Gdyby coś takiego zostało poprawnie wbudowaneIEnumerable
, byłoby możliwe poproszenie listy o „zrobienie migawki” o wszelkich aspektach, które mogą się zmienić w przyszłości bez konieczności generowania wszystkiego.Poniższy kod:
Jest niczym, ponieważ z powodu leniwej oceny nic się nie stanie.
Jest inaczej, ponieważ ocena zostanie uruchomiona.
Każdy przedmiot
mythings
zostanie przekazany pierwszemuWhere
. Jeśli przejdzie, zostanie przekazany drugiemuWhere
. Jeśli przejdzie, będzie częścią wyjścia.Wygląda to mniej więcej tak:
źródło
Odłóż na bok wykonanie (które już wyjaśniają inne odpowiedzi, po prostu podkreślę inny szczegół), bardziej przypomina to twój drugi przykład.
Miejmy tylko wyobrazić zadzwonić
ToList
nathings
.Realizacja
Enumerable.Where
zwrotów aEnumerable.WhereListIterator
. Kiedy wywołujeszWhere
toWhereListIterator
(inaczej połączenie łańcuchoweWhere
), nie dzwonisz jużEnumerable.Where
, aleEnumerable.WhereListIterator.Where
, co faktycznie łączy predykaty (używanieEnumerable.CombinePredicates
).Więc to bardziej jak
if(t.IsSomeValue && t.IsSomeOtherValue)
.źródło
Nie, to nie to samo. W twoim przykładzie
things
jestIEnumerable
, który w tym momencie jest nadal tylko iteratorem, a nie rzeczywistą tablicą lub listą. Ponadto, ponieważthings
nie jest używany, pętla nigdy nie jest nawet oceniana. Ten typIEnumerable
pozwala na iterację elementów -yield
według instrukcji Linq i przetwarzanie ich dalej z większą liczbą instrukcji, co oznacza, że w końcu naprawdę masz tylko jedną pętlę.Ale jak tylko dodasz instrukcję taką jak
.ToArray()
lub.ToList()
, zamawiasz utworzenie rzeczywistej struktury danych, tym samym ograniczając swój łańcuch.Zobacz to powiązane pytanie SO: /programming/2789389/how-do-i-implement-ienumerable
źródło