IEnumerable vs List - Що використовувати? Як вони працюють?


677

У мене є деякі сумніви з приводу того, як працюють переписувачі та LINQ. Розглянемо ці два прості вибір:

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

або

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

Я змінив назви своїх оригінальних об'єктів, щоб це виглядало як більш загальний приклад. Сам запит не так важливий. Я хочу запитати:

foreach (Animal animal in sel) { /*do stuff*/ }
  1. Я помітив, що якщо я використовую IEnumerable, коли налагоджую і перевіряю "sel", який у даному випадку є IEnumerable, він має кілька цікавих членів: "внутрішній", "зовнішній", "innerKeySelector" і "externalKeySelector", ці останні 2 з'являються бути делегатами. "Внутрішній" член не має в ньому примірників "Тварина", а, наприклад, "Видові" екземпляри, що для мене було дуже дивно. "Зовнішній" член містить екземпляри "Animal". Я припускаю, що два делегати визначають, що входить і що з цього виходить?

  2. Я помітив, що якщо я використовую "Розрізнення", "внутрішній" містить 6 елементів (це невірно, оскільки лише 2 - "Відмінність", але "зовнішній" містить правильні значення. Знову ж таки, ймовірно, делеговані методи визначають це, але це трохи більше, ніж я знаю про IEnumerable.

  3. Найголовніше, який із двох варіантів є найкращим?

Конвертація злого списку через .ToList()?

Чи, можливо, безпосередньо користуватися перелічником?

Якщо ви можете, будь-ласка, поясніть також трохи або перекиньте кілька посилань, які пояснюють це використання IEnumerable.

Відповіді:


739

IEnumerableописує поведінку, тоді як Список - це реалізація такої поведінки. Під час використання IEnumerableви даєте компілятору можливість відкласти роботу на пізніше, можливо оптимізуючи її на цьому шляху. Якщо ви використовуєте ToList (), ви змушуєте компілятора негайно повторити результати.

Щоразу, коли я "укладаю" вирази LINQ, я використовую IEnumerable, тому що, лише уточнюючи поведінку, я даю LINQ можливість відкласти оцінку та, можливо, оптимізувати програму. Пам'ятайте, як LINQ не генерує SQL для запиту бази даних, поки ви не перерахуєте його? Врахуйте це:

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;
}

Тепер у вас є метод, який вибирає початковий зразок ("AllSpotted"), а також деякі фільтри. Отже, тепер ви можете зробити це:

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

Тож чи швидше використовувати List over IEnumerable? Тільки якщо ви хочете запобігти виконанню запиту не один раз. Але чи краще в цілому? Добре у вищесказаному Leopards та Hyenas перетворюються на поодинокі SQL запити , а база даних повертає лише ті релевантні рядки. Але якби ми повернули Список AllSpotted(), то він може працювати повільніше, оскільки база даних може повернути набагато більше даних, ніж насправді потрібно, і ми витрачаємо цикли, здійснюючи фільтрацію в клієнті.

У програмі може бути краще відкласти перетворення запиту до списку до самого кінця, тому, якщо я збираюся перераховувати через Леопарди та Гієни не один раз, я б це зробив:

List<Animals> Leopards = Feline(AllSpotted()).ToList();
List<Animals> Hyenas = Canine(AllSpotted()).ToList();

11
Я думаю, що вони посилаються на дві сторони об'єднання. Якщо ви робите "ВИБРАТИ * З Тварин приєднуйтесь до видів ...", то внутрішня частина з'єднання - це Тварини, а зовнішня - Види.
Кріс Венхем

10
Коли я прочитав відповіді про: IEnumerable <T> vs IQueryable <T>, я побачив аналогічне пояснення, так що IEnumerable автоматично змушує час виконання використовувати LINQ для об’єктів для запиту колекції. Тому я плутаюся між цими 3 типами. stackoverflow.com/questions/2876616 / ...
Бронек

4
@Bronek Відповідь, яку ви пов’язали, правдива. IEnumerable<T>буде LINQ-To-Objects після того, як перша частина означає, що все помічене потрібно повернути для запуску Feline. З іншого боку, програма IQuertable<T>дозволить уточнити запит, витягнувши лише плямисті коти.
Нейт

21
Ця відповідь дуже вводить в оману! @ Коментар Нейт пояснює, чому. Якщо ви використовуєте IEnumerable <T>, фільтр відбудеться на стороні клієнта незалежно від того.
Ганс

5
Так, AllSpotted () буде запускатися двічі. Більшою проблемою у цій відповіді є наступне твердження: "Ну що у вищесказаному, Leopards та Hyenas перетворюються в поодинокі SQL запити, а база даних повертає лише ті релевантні рядки." Це помилково, оскільки виклик де викликається на IEnumerable <> і знає лише, як пройти цикл через об'єкти, які вже надходять із бази даних. Якщо ви повернули AllSpotted () та параметри Feline () та Canine () в IQueryable, то фільтр відбудеться в SQL, і ця відповідь мала б сенс.
Ганс

178

Тут є дуже хороша стаття, яку написав TechBlog Клаудіо Бернасконі: Коли користуватися IEnumerable, ICollection, IList та List

Ось деякі основні моменти щодо сценаріїв та функцій:

введіть тут опис зображення введіть тут опис зображення


25
Слід зазначити, що ця стаття стосується лише загальнодоступних частин вашого коду, а не внутрішньої роботи. Listє реалізація IListі як такий має додаткові функціональні можливості понад ті , що в IList(наприклад Sort, Find, InsertRange). Якщо ви IListList
змусите

4
Не забувайтеIReadOnlyCollection<T>
Dandré

2
Тут може бути корисно включити і звичайний масив [].
jbyrd

Хоча це може бути засуджено, спасибі за обмін цієї графіки і статті
Daniel

133

Клас, який реалізує, IEnumerableдозволяє використовувати foreachсинтаксис.

В основному він має метод отримати наступний елемент колекції. Йому не потрібно, щоб вся колекція була в пам’яті і не знає, скільки предметів у ній, foreachпросто продовжує отримувати наступний предмет, поки його не закінчиться.

Це може бути дуже корисно за певних обставин, наприклад, у масивній таблиці баз даних ви не хочете копіювати всю річ у пам'ять перед тим, як розпочати обробку рядків.

Зараз Listреалізує IEnumerable, але представляє всю колекцію в пам'яті. Якщо у вас є IEnumerableі ви телефонуєте, .ToList()ви створюєте новий список із вмістом перерахунку в пам'яті.

Вираз linq повертає перерахунок, і за замовчуванням вираз виконується під час ітерації за допомогою foreach. An IEnumerableвиконується оператор LINQ , коли ви ітеріруете foreach, але ви можете змусити його ітерацію швидше , використовуючи .ToList().

Ось що я маю на увазі:

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 ) ...

2
Але що станеться, якщо виконувати foreach на IEnumerable, попередньо не перетворюючи його у Список ? Це приносить всю колекцію в пам’ять? Або, якщо він створює елемент по черзі, коли він переходить через петлю передбачення? дякую
Pap

@Помініть останнє: воно виконується знову, нічого не зберігається автоматично в пам'яті.
Кіт

здається, що ключовим розбігом є 1) вся справа в пам'яті чи ні. 2) Численні дозвольте мені використовувати, foreachпоки список буде проходити через індекс сказати. Тепер, якщо я хотів би знати кол / довжина від thingзаздалегідь, IEnumerable не допоможе, НЕ так?
Jeb50

@ Jeb50 Не точно - і те, Listі Arrayреалізувати IEnumerable. Ви можете вважати IEnumerableнайменшим загальним знаменником, який працює як для колекцій пам'яті, так і для великих, які отримують по одному предмету за раз. Коли ви телефонуєте, IEnumerable.Count()ви можете зателефонувати у швидку .Lengthвласність або пройти всю колекцію - справа в тому, що з IEnumerableвами не знаєте. Це може бути проблемою, але якщо ви тільки збираєтесь foreachце тоді ви не дбаєте - ваш код буде працювати з Arrayабо DataReaderте ж саме.
Кіт

1
@MFouadKajj Я не знаю, який стек ви використовуєте, але майже напевно це не робить запит у кожному рядку. Сервер виконує запит і обчислює початкову точку набору результатів, але не отримує всю справу. Для невеликих наборів результатів це може бути одна поїздка, для великих ви надсилаєте запит на отримання більше рядків з результатів, але це не повторно виконує весь запит.
Кіт

97

Ніхто не згадав про одну вирішальну різницю, іронічно відповів на запитання, закрите як дублювання цього.

IEumerable є лише для читання, а List - ні.

Див. Практична різниця між List та IEnumerable


Надалі, це через аспект інтерфейсу чи через аспект списку? тобто IList також читається тільки?
Майстер Джейсона

IList не є лише для читання - docs.microsoft.com/en-us/dotnet/api/… IEumerable є лише для читання, оскільки йому не вистачає жодних методів додавання чи видалення будь-якого, як тільки він побудований, це один з базових інтерфейсів, який IList розширюється (див. Посилання)
CAD

67

Найголовніше, що потрібно усвідомити, це те, що, використовуючи Linq, запит не оцінюється відразу. Він виконується лише як частина ітерації через результат, який IEnumerable<T>виходить в foreach- ось що роблять усі дивні делегати.

Отже, перший приклад оцінює запит негайно, зателефонувавши ToListта поклавши результати запиту в список.
Другий приклад повертає таблицю, IEnumerable<T>яка містить всю інформацію, необхідну для подальшого запуску запиту.

Щодо продуктивності, відповідь - це залежить . Якщо вам потрібно оцінити результати одразу (скажімо, ви мутуєте структури, про які ви запитуєте пізніше, або якщо ви не хочете, щоб ітерація протягом IEnumerable<T>тривала тривалий час), скористайтеся списком. В іншому користуйтеся IEnumerable<T>. За замовчуванням має бути використання оцінки за запитом у другому прикладі, оскільки в цілому використовується менше пам'яті, якщо немає конкретної причини для зберігання результатів у списку.


Привіт і дякую за відповідь :: -). Це очистило майже всі мої сумніви. Будь-яка ідея, чому «Перелічене» «розбивається» на «внутрішнє» та «зовнішнє»? Це відбувається, коли я перевіряю елемент в режимі налагодження / перерви за допомогою миші. Це, можливо, внесок Visual Studio? Перерахування на місці та вказівка ​​на вхід та вихід Enum?
Аксон

5
Це Joinробиться його робота - внутрішня і зовнішня - це дві сторони з'єднання. Як правило, не турбуйтеся про те, що насправді є IEnumerables, оскільки це буде абсолютно відрізнятися від вашого фактичного коду. Тільки хвилюйтеся про фактичний вихід, коли ви повторите його :)
thecoop

40

Перевагою IEnumerable є відкладене виконання (зазвичай з базами даних). Запит не буде виконаний, поки ви фактично не пропустите дані. Це запит, який чекає, поки він не знадобиться (він також лінивий завантаження).

Якщо ви зателефонуєте в ToList, запит буде виконаний, або "матеріалізований", як я хочу сказати.

У обох є плюси і мінуси. Якщо ви зателефонуєте в ToList, ви можете видалити таємницю щодо виконання запиту. Якщо ви дотримуєтесь IEnumerable, ви отримуєте перевагу в тому, що програма не виконує жодних робіт, поки це фактично не потрібно.


25

Я поділюсь однією зловживаною концепцією, яку я потрапив за один день:

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() );

Очікуваний результат

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

Фактичний результат

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

Пояснення

Відповідно до інших відповідей, оцінка результату була відкладена до виклику ToListабо подібних методів виклику, наприклад ToArray.

Тому я можу переписати код у цьому випадку як:

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() );

Грайте навколо

https://repl.it/E8Ki/0


1
Це пов'язано з методами linq (розширення), які в цьому випадку надходять з IEnumerable, де створюють лише запит, але не виконують його (за лаштунками використовуються дерева виразів). Таким чином у вас є можливість зробити багато речей із цим запитом, не торкаючись даних (у цьому випадку дані у списку). Метод списку приймає підготовлений запит і виконує його відносно джерела даних.
Бронек

2
Насправді я прочитав усі відповіді, і ваша була та, яку я підняв на голосування, оскільки це чітко констатує різницю між ними, не конкретно розмовляючи про LINQ / SQL. Важливо знати все це, перш ніж ви перейдете до LINQ / SQL. Помилуйся.
BeemerGuy

Це важлива різниця для пояснення, але "очікуваний результат" насправді не очікується. Ти кажеш, що це якась ґутка, а не дизайн.
Нема

@Neme, так, це було моє сподівання, перш ніж я зрозумів, як IEnumerableпрацює, але зараз не більше, оскільки я знаю, як;)
amd

15

Якщо ви хочете лише перелічити їх, скористайтеся IEnumerable.

Однак остерігайтеся, що зміна первісної колекції, яка перераховується, є небезпечною операцією - у цьому випадку вам потрібно ToListспершу. Це створить новий елемент списку для кожного елемента в пам'яті, перерахувавши IEnumerableі, таким чином, менш ефективно, якщо ви перераховуєте лише один раз - але більш безпечні, а іноді Listметоди зручні (наприклад, у випадковому доступі).


1
Я не впевнений, що можна сказати, що створення списку означає нижчу ефективність.
Стівен Судіт

@ Стівен: насправді, як сказав Thecoop і Кріс, іноді може знадобитися використання Списку. У моєму випадку я зробив висновок, що це не так. @ Daren: що ти маєш на увазі під цим "це створить новий список для кожного елемента в пам'яті"? Можливо, ви мали на увазі "запис у списку"? :: -).
Ексон

@Axonn так, я запису списку. фіксований.
Дарен Томас

@Steven Якщо ви плануєте переглядати елементи в елементах IEnumerable, то спочатку створення списку (і повторення над цим) означає, що ви повторюєте елементи двічі . Тому, якщо ви не хочете виконувати ефективніші в списку операції, це насправді означає низьку ефективність.
Дарен Томас

3
@jerhewet: ніколи не є корисною зміною послідовності, яка повторюється. Погані речі трапляться. Абстракції будуть просочуватися. Демони прорвуться в наш вимір і загрожують хаосом. Так що так, .ToList()допомагає тут;)
Дарен Томас

5

Окрім усіх відповідей, розміщених вище, ось мої два центи. Існує багато інших типів, крім List, який реалізує IE Численні такі ICollection, ArrayList і т. Д. Отже, якщо у нас є IEnumerable як параметр будь-якого методу, ми можемо передавати функції будь-яких типів колекції. Тобто ми можемо мати метод для роботи над абстракцією, а не якусь конкретну реалізацію.


1

Існує багато випадків (наприклад, нескінченний список або дуже великий список), коли IEnumerable не може бути перетворений на Список. Найочевидніші приклади - це всі прості номери, всі користувачі facebook зі своїми реквізитами або всі елементи на ebay.

Різниця полягає в тому, що об’єкти "Список" зберігаються "прямо тут і зараз", тоді як "IEnumeful" об'єкти працюють "лише один за одним". Тож якщо я переглядаю всі предмети на ebay, один за одним буде чимось, з чим навіть малий комп’ютер може впоратися, але ".ToList ()", безумовно, вичерпає мені пам'ять, незалежно від того, наскільки великий мій комп'ютер. Жоден комп’ютер сам по собі не може містити та обробляти таку величезну кількість даних.

[Редагувати] - Потрібно говорити, що це "не те чи те". часто було б доцільно використовувати і список, і IEnumerable в одному класі. Жоден комп’ютер у світі не міг перерахувати всі прості числа, тому що за визначенням це вимагало б нескінченної кількості пам'яті. Але ви можете легко придумати, class PrimeContainerщо містить IEnumerable<long> primes, який із зрозумілих причин також містить SortedList<long> _primes. всі проліки, що розраховані досі. наступний прайм, який слід перевірити, буде виконуватися лише проти існуючих праймерів (до квадратного кореня). Таким чином ви отримуєте обидва - праймери один за одним (IEnumerable) і хороший список "прайменів поки що", що є досить гарним наближенням до всього (нескінченного) списку.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.