IEnumerable vs List - Czego używać? Jak oni pracują?

677

Mam wątpliwości co do działania Enumeratorów i LINQ. Rozważ te dwa proste wybory:

List<Animal> sel = (from animal in Animals 
                    join race in Species
                    on animal.SpeciesKey equals race.SpeciesKey
                    select animal).Distinct().ToList();

lub

IEnumerable<Animal> sel = (from animal in Animals 
                           join race in Species
                           on animal.SpeciesKey equals race.SpeciesKey
                           select animal).Distinct();

Zmieniłem nazwy moich oryginalnych obiektów, aby wyglądało to na bardziej ogólny przykład. Samo zapytanie nie jest tak ważne. Chcę zapytać o to:

foreach (Animal animal in sel) { /*do stuff*/ }
  1. Zauważyłem, że jeśli używam IEnumerable, kiedy debuguję i sprawdzam „sel”, którym w tym przypadku jest IEnumerable, ma on kilka interesujących elementów: „wewnętrzny”, „zewnętrzny”, „innerKeySelector” i „outerKeySelector”, pojawiają się te 2 ostatnie być delegatami. Członek „wewnętrzny” nie ma w nim instancji „Zwierzęcych”, lecz instancje „Gatunków”, co było dla mnie bardzo dziwne. Element „zewnętrzny” zawiera instancje „Zwierząt”. Zakładam, że dwaj delegaci określają, kto wchodzi i co wychodzi?

  2. Zauważyłem, że jeśli użyję „Wyraźny”, „wewnętrzny” zawiera 6 elementów (jest to niepoprawne, ponieważ tylko 2 są Wyróżniające), ale „zewnętrzny” zawiera prawidłowe wartości. Znów, prawdopodobnie delegowane metody to określają, ale jest to nieco więcej niż wiem o IEnumerable.

  3. Co najważniejsze, która z dwóch opcji jest najlepsza pod względem wydajności?

Konwersja listy złych przez .ToList()?

A może bezpośrednio za pomocą modułu wyliczającego?

Jeśli możesz, proszę wytłumacz trochę lub wrzuć linki, które wyjaśniają użycie IEnumerable.

Axonn
źródło

Odpowiedzi:

739

IEnumerableopisuje zachowanie, podczas gdy List jest implementacją tego zachowania. Kiedy używasz IEnumerable, dajesz kompilatorowi szansę na odłożenie pracy na później, ewentualnie optymalizując po drodze. Jeśli używasz ToList (), zmuszasz kompilator do natychmiastowego potwierdzenia wyników.

Ilekroć „układam” wyrażenia LINQ, używam IEnumerable, ponieważ tylko określając zachowanie, daję LINQ szansę na odroczenie oceny i ewentualnie optymalizację programu. Pamiętasz, jak LINQ nie generuje kodu SQL do wysłania zapytania do bazy danych, dopóki go nie policzysz? Rozważ to:

public IEnumerable<Animals> AllSpotted()
{
    return from a in Zoo.Animals
           where a.coat.HasSpots == true
           select a;
}

public IEnumerable<Animals> Feline(IEnumerable<Animals> sample)
{
    return from a in sample
           where a.race.Family == "Felidae"
           select a;
}

public IEnumerable<Animals> Canine(IEnumerable<Animals> sample)
{
    return from a in sample
           where a.race.Family == "Canidae"
           select a;
}

Teraz masz metodę, która wybiera próbkę początkową („AllSpotted”) oraz kilka filtrów. Teraz możesz to zrobić:

var Leopards = Feline(AllSpotted());
var Hyenas = Canine(AllSpotted());

Czy korzystanie z Listy jest szybsze IEnumerable? Tylko jeśli chcesz zapobiec wykonywaniu zapytania więcej niż raz. Ale czy ogólnie jest lepiej? W powyższym przypadku lamparty i hieny są konwertowane na pojedyncze zapytania SQL , a baza danych zwraca tylko odpowiednie wiersze. Ale jeśli zwróciliśmy Listę AllSpotted(), może działać wolniej, ponieważ baza danych może zwrócić znacznie więcej danych, niż jest w rzeczywistości potrzebna, i marnujemy cykle na filtrowaniu w kliencie.

W programie może być lepiej odłożyć konwersję zapytania na listę do samego końca, więc jeśli mam zamiar wyliczyć Leopardy i Hieny więcej niż raz, zrobiłbym to:

List<Animals> Leopards = Feline(AllSpotted()).ToList();
List<Animals> Hyenas = Canine(AllSpotted()).ToList();
Chris Wenham
źródło
11
Myślę, że odnoszą się one do dwóch stron złączenia. Jeśli wykonasz „WYBIERZ * Z Animals DOŁĄCZ Gatunek ...”, wówczas wewnętrzna część połączenia to Zwierzęta, a zewnętrzna to Gatunek.
Chris Wenham
10
Kiedy przeczytałem odpowiedzi na temat: IEnumerable <T> vs IQueryable <T> zobaczyłem analogiczne wyjaśnienie, więc IEnumerable automatycznie zmusza środowisko wykonawcze do korzystania z LINQ do Objects w celu przeszukiwania kolekcji. Więc jestem zdezorientowany między tymi 3 typami. stackoverflow.com/questions/2876616/…
Bronek
4
@Bronek Odpowiedź, którą podałeś, jest prawdą. IEnumerable<T>będzie LINQ-To-Objects po pierwszej części, co oznacza, że ​​wszystkie wykryte będą musiały zostać zwrócone, aby uruchomić Feline. Z drugiej strony IQuertable<T>pozwoli dopracować zapytanie, ściągając w dół tylko Feliny.
Nate
21
Ta odpowiedź jest bardzo myląca! Komentarz @ Nate wyjaśnia dlaczego. Jeśli używasz IEnumerable <T>, filtr będzie działał po stronie klienta bez względu na wszystko.
Hans
5
Tak AllSpotted () będzie uruchamiany dwukrotnie. Większym problemem związanym z tą odpowiedzią jest następujące stwierdzenie: „W powyższym przypadku Leopardy i Hieny są konwertowane na pojedyncze zapytania SQL, a baza danych zwraca tylko odpowiednie wiersze”. Jest to nieprawda, ponieważ klauzula where jest wywoływana w IEnumerable <> i która wie tylko, jak zapętlać obiekty, które już pochodzą z bazy danych. Jeśli zwróciłeś AllSpotted () i parametry Feline () i Canine () do IQueryable, to filtr pojawiłby się w SQL i ta odpowiedź miałaby sens.
Hans,
178

Jest bardzo dobry artykuł napisany przez: TechBlog Claudio Bernasconiego tutaj: Kiedy używać IEnumerable, ICollection, IList i List

Oto kilka podstawowych informacji na temat scenariuszy i funkcji:

wprowadź opis zdjęcia tutaj wprowadź opis zdjęcia tutaj

rubStackOverflow
źródło
25
Należy zauważyć, że ten artykuł dotyczy tylko części kodu umieszczonych w miejscach publicznych, a nie wewnętrznych. ListJest to realizacja IListi jako taki posiada dodatkową funkcjonalność na górze w tych IList(np Sort, Find, InsertRange). Jeśli zmusić się do wykorzystania IListw ciągu List, tracisz te metody, które mogą wymagać
Jonathan Twite
4
Nie zapomnijIReadOnlyCollection<T>
Dandré,
2
Pomocne może być również włączenie zwykłej tablicy [].
jbyrd
Choć może się to nie podobać, dziękuję za udostępnienie tej grafiki i artykułu
Daniel
133

Klasa, która implementuje IEnumerablepozwala na użycie foreachskładni.

Zasadniczo ma metodę uzyskania następnego elementu w kolekcji. Nie potrzebuje całej kolekcji do zapamiętania i nie wie, ile jest w niej przedmiotów, foreachpo prostu dostaje następny element, aż skończy się.

Może to być bardzo przydatne w pewnych okolicznościach, na przykład w ogromnej tabeli bazy danych, w której nie chcesz kopiować całej rzeczy do pamięci przed rozpoczęciem przetwarzania wierszy.

Teraz Listimplementuje IEnumerable, ale reprezentuje całą kolekcję w pamięci. Jeśli masz IEnumerablei dzwonisz .ToList(), tworzysz nową listę z zawartością wyliczenia w pamięci.

Wyrażenie linq zwraca wyliczenie, a domyślnie wyrażenie jest wykonywane podczas iteracji za pomocą foreach. An IEnumerableLINQ Wykonuje oświadczenie gdy iteracyjne foreach, ale można zmusić go do iteracji prędzej użyciu .ToList().

Oto co mam na myśli:

var things = 
    from item in BigDatabaseCall()
    where ....
    select item;

// this will iterate through the entire linq statement:
int count = things.Count();

// this will stop after iterating the first one, but will execute the linq again
bool hasAnyRecs = things.Any();

// this will execute the linq statement *again*
foreach( var thing in things ) ...

// this will copy the results to a list in memory
var list = things.ToList()

// this won't iterate through again, the list knows how many items are in it
int count2 = list.Count();

// this won't execute the linq statement - we have it copied to the list
foreach( var thing in list ) ...
Keith
źródło
2
Ale co się stanie, jeśli uruchomisz foreach na IEnumerable bez konwersji najpierw do listy ? Czy przywołuje całą kolekcję do pamięci? Czy też tworzy instancję elementu jeden po drugim, iterując po pętli foreach? dzięki
Pap
@Pap to drugie: wykonuje się ponownie, nic nie jest automatycznie buforowane w pamięci.
Keith
wygląda na to, że kluczową różnicą jest 1) cała pamięć lub nie. 2) IEnumerable pozwól mi użyć, foreachpodczas gdy List będzie przechodzić przez powiedz indeks. Teraz, jeśli chciałbym poznać count / długość z thingwyprzedzeniem, IEnumerable nie pomoże, prawda?
Jeb50
@ Jeb50 Niezupełnie - oba Listi Arrayimplementacja IEnumerable. Możesz myśleć o IEnumerablenajniższym wspólnym mianowniku, który działa zarówno w kolekcjach pamięci, jak i dużych, które otrzymują po jednym elemencie na raz. Kiedy dzwonisz, IEnumerable.Count()możesz dzwonić do szybkiej .Lengthnieruchomości lub przeglądać całą kolekcję - chodzi o to, że z IEnumerabletobą nie wiesz. To może być problem, ale jeśli tylko będzie foreachto wtedy nie obchodzi - kod będzie pracować z Arraylub DataReadertakie same.
Keith,
1
@MFouadKajj Nie wiem, jakiego stosu używasz, ale prawie na pewno nie wysyła żądania do każdego wiersza. Serwer uruchamia zapytanie i oblicza punkt początkowy zestawu wyników, ale nie otrzymuje całości. W przypadku małych zestawów wyników może to być pojedyncza podróż, w przypadku dużych wysyłane jest żądanie o więcej wierszy z wyników, ale nie uruchamia ponownie całego zapytania.
Keith,
97

Nikt nie wspomniał o jednej istotnej różnicy, na ironię odpowiedział na pytanie zamknięte jako powielenie tego.

IEnumerable jest tylko do odczytu, a lista nie.

Zobacz Praktyczna różnica między List a IEnumerable

CAD CAD
źródło
Czy w związku z tym wynika to z aspektu interfejsu czy z powodu listy? tzn. czy IList jest również do odczytu?
Jason Masters
IList nie jest tylko do odczytu - docs.microsoft.com/en-us/dotnet/api/... IEnumerable jest tylko do odczytu, ponieważ nie ma żadnych metod dodawania lub usuwania czegokolwiek po zbudowaniu, jest to jeden z podstawowych interfejsów, który IList rozszerza się (patrz link)
facet CAD
67

Najważniejszą rzeczą do zrozumienia jest to, że przy użyciu Linq zapytanie nie jest natychmiast oceniane. Jest prowadzony wyłącznie jako część iteracja uzyskany IEnumerable<T>w sposób foreach- to wszystko dziwne delegaci robią.

Tak więc pierwszy przykład ocenia zapytanie natychmiast, wywołując ToListi umieszczając wyniki zapytania na liście.
Drugi przykład zwraca an IEnumerable<T>zawierający wszystkie informacje potrzebne do późniejszego uruchomienia zapytania.

Pod względem wydajności odpowiedź jest zależna . Jeśli potrzebujesz, aby wyniki zostały ocenione od razu (powiedzmy, że mutujesz struktury, o które pytasz później, lub jeśli nie chcesz, aby iteracja IEnumerable<T>trwała dłużej), użyj listy. W przeciwnym razie użyj IEnumerable<T>. Domyślnie należy użyć oceny na żądanie w drugim przykładzie, ponieważ generalnie zużywa ona mniej pamięci, chyba że istnieje konkretny powód do przechowywania wyników na liście.

Thecoop
źródło
Cześć i dziękuję za odpowiedź :: -). To rozwiało prawie wszystkie moje wątpliwości. Masz pojęcie, dlaczego Wyliczalny jest „podzielony” na „wewnętrzny” i „zewnętrzny”? Dzieje się tak, gdy sprawdzam element w trybie debugowania / przerywania za pomocą myszy. Czy to może wkład Visual Studio? Wyliczanie na miejscu i wskazanie wejścia i wyjścia Enum?
Axonn
5
Na Jointym polega praca - wewnętrzna i zewnętrzna to dwie strony złączenia. Zasadniczo nie martw się o to, co jest w rzeczywistości IEnumerables, ponieważ będzie to zupełnie inne niż twój kod. Martw się tylko o rzeczywistą moc wyjściową podczas iteracji :)
Thecoop
40

Zaletą IEnumerable jest odroczenie wykonania (zwykle z bazami danych). Zapytanie nie zostanie wykonane, dopóki dane nie zostaną zapętlone. To zapytanie czeka, aż będzie potrzebne (inaczej leniwe ładowanie).

Jeśli wywołasz ToList, zapytanie zostanie wykonane lub „zmaterializowane”, jak lubię mówić.

Oba mają swoje zalety i wady. Jeśli wywołasz ToList, możesz usunąć tajemnicę, kiedy zapytanie zostanie wykonane. Jeśli pozostaniesz przy IEnumerable, zyskasz tę zaletę, że program nie wykona żadnej pracy, dopóki nie będzie faktycznie wymagany.

Matt Sherman
źródło
25

Podzielę się jedną nadużywaną koncepcją, do której wpadłem jednego dnia:

var names = new List<string> {"mercedes", "mazda", "bmw", "fiat", "ferrari"};

var startingWith_M = names.Where(x => x.StartsWith("m"));

var startingWith_F = names.Where(x => x.StartsWith("f"));


// updating existing list
names[0] = "ford";

// Guess what should be printed before continuing
print( startingWith_M.ToList() );
print( startingWith_F.ToList() );

Spodziewany wynik

// I was expecting    
print( startingWith_M.ToList() ); // mercedes, mazda
print( startingWith_F.ToList() ); // fiat, ferrari

Aktualny rezultat

// what printed actualy   
print( startingWith_M.ToList() ); // mazda
print( startingWith_F.ToList() ); // ford, fiat, ferrari

Wyjaśnienie

Podobnie jak w przypadku innych odpowiedzi, ocena wyniku została odroczona do momentu ToListna przykład wywołania lub podobnych metod wywołania ToArray.

W tym przypadku mogę przepisać kod jako:

var names = new List<string> {"mercedes", "mazda", "bmw", "fiat", "ferrari"};

// updating existing list
names[0] = "ford";

// before calling ToList directly
var startingWith_M = names.Where(x => x.StartsWith("m"));

var startingWith_F = names.Where(x => x.StartsWith("f"));

print( startingWith_M.ToList() );
print( startingWith_F.ToList() );

Graj wokół

https://repl.it/E8Ki/0

amd
źródło
1
Wynika to z metod linq (rozszerzenie), które w tym przypadku pochodzą z IEnumerable, w którym tylko tworzą zapytanie, ale go nie wykonują (za kulisami używane są drzewa wyrażeń). W ten sposób masz możliwość wykonywania wielu czynności za pomocą tego zapytania bez dotykania danych (w tym przypadku danych na liście). Metoda list pobiera przygotowane zapytanie i wykonuje je względem źródła danych.
Bronek
2
Właściwie to przeczytałem wszystkie odpowiedzi, a wasza była tą, którą głosowałem, ponieważ wyraźnie określa różnicę między nimi, nie mówiąc konkretnie o LINQ / SQL. Koniecznie musisz znać to wszystko PRZED przejściem do LINQ / SQL. Podziwiać.
BeemerGuy
To istotna różnica do wyjaśnienia, ale tak naprawdę nie oczekuje się „oczekiwanego wyniku”. Mówisz, że to raczej gotcha niż projekt.
Neme
@Neme, tak To było moje oczekiwania przed rozumiem, jak IEnumerableto działa, ale teraz nie jest bardziej ponieważ wiem jak;)
amd
15

Jeśli wszystko, co chcesz zrobić, to wymienić je, użyj IEnumerable.

Uważaj jednak, że zmiana wyliczanego zbioru oryginalnego jest niebezpieczną operacją - w tym przypadku będziesz chciał ToListnajpierw. Spowoduje to utworzenie nowego elementu listy dla każdego elementu w pamięci, wyliczając IEnumerablei dlatego jest mniej wydajny, jeśli wyliczysz tylko raz - ale bezpieczniejsze, a czasem Listmetody są przydatne (na przykład w przypadkowym dostępie).

Daren Thomas
źródło
1
Nie jestem pewien, czy można bezpiecznie powiedzieć, że wygenerowanie listy oznacza niższą wydajność.
Steven Sudit
@ Steven: w rzeczy samej, jak powiedział Thecoop i Chris, czasami może być konieczne użycie Listy. W moim przypadku doszedłem do wniosku, że tak nie jest. @ Daren: co rozumiesz przez „spowoduje to utworzenie nowej listy dla każdego elementu w pamięci”? Może miałeś na myśli „wpis na liście”? :: -).
Axonn
@Axonn tak, mam na myśli wpis listy. naprawiony.
Daren Thomas
@Steven Jeśli planujesz iterację elementów w IEnumerable, to najpierw utworzenie listy (i iteracja nad nią) oznacza, że ​​powtórzysz dwa elementy . Jeśli więc nie chcesz wykonywać operacji, które są bardziej wydajne na liście, oznacza to naprawdę niższą wydajność.
Daren Thomas
3
@jerhewet: modyfikowanie powtarzanej sekwencji nigdy nie jest dobrym pomysłem. Staną się złe rzeczy. Wyciekną abstrakcje. Demony włamią się do naszego wymiaru i sieją spustoszenie. Więc tak, .ToList()pomaga tutaj;)
Daren Thomas
5

Oprócz wszystkich odpowiedzi zamieszczonych powyżej, oto moje dwa centy. Istnieje wiele innych typów niż List, które implementują IEnumerable, takie jak ICollection, ArrayList itp. Więc jeśli mamy IEnumerable jako parametr dowolnej metody, możemy przekazać dowolne typy kolekcji do funkcji. Tj. Możemy mieć metodę działania na abstrakcji, a nie na konkretnej implementacji.

Ananth
źródło
1

Istnieje wiele przypadków (takich jak lista nieskończona lub bardzo duża), w których IEnumerable nie może zostać przekształcony w Listę. Najbardziej oczywistymi przykładami są wszystkie liczby pierwsze, wszyscy użytkownicy Facebooka z ich szczegółami lub wszystkie przedmioty w serwisie eBay.

Różnica polega na tym, że obiekty „Lista” są przechowywane „tu i teraz”, podczas gdy „IEnumerable” obiekty działają „tylko jeden na raz”. Więc jeśli przeglądam wszystkie elementy w serwisie eBay, jeden na raz byłby czymś, co poradziłby sobie nawet mały komputer, ale „.ToList ()” z pewnością zabrakłoby mi pamięci, bez względu na to, jak duży był mój komputer. Żaden komputer nie może sam w sobie zawierać i przetwarzać tak dużej ilości danych.

[Edytuj] - Nie trzeba dodawać - to nie jest „ani to, ani tamto”. często miałoby sens zastosowanie zarówno listy, jak i IEnumerable w tej samej klasie. Żaden komputer na świecie nie może wyświetlić wszystkich liczb pierwszych, ponieważ z definicji wymagałoby to nieskończonej ilości pamięci. Ale możesz łatwo pomyśleć o tym, class PrimeContainerktóry zawiera IEnumerable<long> primes, który z oczywistych powodów również zawiera SortedList<long> _primes. wszystkie liczby pierwsze obliczone do tej pory. następna liczba pierwsza, która zostanie sprawdzona, zostanie uruchomiona tylko na podstawie istniejących liczb pierwszych (do pierwiastka kwadratowego). W ten sposób zyskujesz zarówno liczby pierwsze pojedynczo (IEnumerable), jak i dobrą listę „liczb pierwszych do tej pory”, co jest całkiem dobrym przybliżeniem całej (nieskończonej) listy.

LongChalk
źródło