IEnumerable vs List - что использовать? Как они работают?

У меня есть некоторые сомнения в том, как работают перечислители и LINQ. Рассмотрим эти два простых варианта выбора:

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

or

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, у него есть несколько интересных членов: "inner", "external", "innerKeySelector" и "outerKeySelector", последние 2 кажутся делегатами. «Внутренний» член не содержит экземпляров «Animal», а скорее экземпляров «Species», что было для меня очень странно. «Внешний» член действительно содержит экземпляры «Animal». Я предполагаю, что два делегата определяют, что входит, а что выходит?

  2. Я заметил, что если я использую «Distinct», «внутренний» содержит 6 элементов (это неверно, поскольку только 2 являются Distinct), но «внешний» действительно содержит правильные значения. Опять же, вероятно, делегированные методы определяют это, но это немного больше, чем я знаю об IEnumerable.

  3. Самое главное, какой из двух вариантов является лучшим с точки зрения производительности?

Преобразование злого списка через .ToList()?

Или, может быть, напрямую использовать перечислитель?

Если вы можете, пожалуйста, также немного объясните или добавьте несколько ссылок, которые объясняют использование IEnumerable.


person Axonn    schedule 02.09.2010    source источник


Ответы (10)


IEnumerable описывает поведение, а List является его реализацией. Когда вы используете 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? Только если вы хотите, чтобы запрос не выполнялся более одного раза. Но так ли лучше в целом? В приведенном выше примере Леопарды и Гиены преобразуются в отдельные запросы SQL каждый, а база данных возвращает только релевантные строки. Но если бы мы вернули список из AllSpotted(), он мог бы работать медленнее, потому что база данных могла бы возвращать гораздо больше данных, чем действительно необходимо, и мы тратим циклы на фильтрацию в клиенте.

В программе может быть лучше отложить преобразование вашего запроса в список до самого конца, поэтому, если я собираюсь перечислять Леопардов и Гиен более одного раза, я бы сделал следующее:

List<Animals> Leopards = Feline(AllSpotted()).ToList();
List<Animals> Hyenas = Canine(AllSpotted()).ToList();
person Chris Wenham    schedule 02.09.2010
comment
Привет и спасибо за ответ :: -). Вы дали мне очень хороший пример того, как очевидно, что случай IEnumerable имеет преимущество в производительности. Есть идеи относительно другой части моего вопроса? Почему Enumerable разделен на внутреннее и внешнее? Это происходит, когда я проверяю элемент в режиме отладки / прерывания с помощью мыши. Возможно, это вклад Visual Studio? Перечисление на месте и указание ввода и вывода Enum? - person Axonn; 02.09.2010
comment
Я думаю, они относятся к двум сторонам соединения. Если вы выполните SELECT * FROM Animals JOIN Species ... тогда внутренняя часть соединения будет Animals, а внешняя часть - Species. - person Chris Wenham; 03.09.2010
comment
Вы также можете использовать скомпилированные запросы в LINQ для дальнейшей оптимизации вашего кода, если хотите. :) - person PmanAce; 24.04.2013
comment
Когда я прочитал ответы на тему: IEnumerable ‹T› vs IQueryable ‹T›, я увидел аналогичное объяснение, так что IEnumerable автоматически заставляет среду выполнения использовать LINQ to Objects для запроса коллекции. Итак, я запутался между этими тремя типами. stackoverflow.com/questions/2876616/ - person Bronek; 07.01.2014
comment
Извините, я немного запутался! Вы хотите сказать, что var Leopards = Feline (AllSpotted ()); будет извлекать данные из базы данных за один раз с реализацией обоих фильтров вместо того, чтобы сначала получить все обнаруженные, а затем только кошачьи ?? - person Lakshay; 26.02.2014
comment
@Lakshay правильно, кроме него, будет выполняться при добавлении и вызове .ToList (). - person Jim; 07.07.2014
comment
@Bronek Ответ, который вы связали, верный. IEnumerable<T> будет LINQ-To-Objects после первой части, что означает, что все обнаруженные объекты должны быть возвращены для запуска Feline. С другой стороны, IQuertable<T> позволит уточнить запрос, выбрав только пятнистых кошачьих. - person Nate; 20.08.2014
comment
Этот ответ вводит в заблуждение! Комментарий @Nate объясняет, почему. Если вы используете IEnumerable ‹T›, фильтр будет работать на стороне клиента, несмотря ни на что. - person Hans; 27.01.2015
comment
Другой альтернативой IEnumerable<T> является IReadOnlyCollection<T>. Причина, по которой я упоминаю об этом, заключается в том, что IEnumerable<T> может относиться к последовательности неопределенного размера, в то время как размер последовательности IReadOnlyCollection<T> является дифференцирующим. Да, IReadOnlyCollection<T> - это IEnumerable<T>, но, по крайней мере, вы знаете, что когда вы получаете IReadOnlyCollection<T>, это своего рода набор, который вы можете повторять несколько раз, в то время как вы можете не быть так уверены, когда получите IEnumerable<T> (он может иметь огромную производительность влияние при повторении). - person Dandré; 10.07.2016
comment
@Hans - Значит ли это, что запрос AllSpotted будет запущен для БД дважды? Или только один раз и запуск Canine () против него, извлеченного из памяти? - person nthpixel; 08.10.2016
comment
Да, AllSpotted () будет запущен дважды. Более серьезной проблемой с этим ответом является следующее утверждение: в приведенном выше примере Леопарды и Гиены преобразуются в отдельные запросы SQL каждый, а база данных возвращает только те строки, которые имеют отношение к делу. Это неверно, потому что предложение where вызывается в IEnumerable ‹›, и он знает только, как перебирать объекты, которые уже поступают из базы данных. Если вы вернули AllSpotted () и параметры Feline () и Canine () в IQueryable, тогда фильтр будет выполняться в SQL, и этот ответ будет иметь смысл. - person Hans; 12.10.2016
comment
Неверный и вводящий в заблуждение ответ. IEnumerable (только для чтения) полезен с ключевым словом yield для отложенного выполнения. Всегда лучше кэшировать результаты в список или словарь для повторного выполнения или произвольного доступа. - person Deepak Mishra; 20.12.2019
comment
За правильное использование слова «reify» в предложении. - person Jazimov; 17.06.2020

Есть очень хорошая статья, написанная: TechBlog Клаудио Бернаскони здесь: Когда использовать IEnumerable, ICollection, IList и List

Вот некоторые основные сведения о сценариях и функциях:

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

person rubStackOverflow    schedule 04.06.2015
comment
Следует отметить, что эта статья предназначена только для общедоступных частей вашего кода, а не для внутренней работы. List является реализацией IList и, как таковой, имеет дополнительные функциональные возможности по сравнению с IList (например, Sort, Find, InsertRange). Если вы заставите себя использовать IList вместо List, вы потеряете эти методы, которые могут вам потребоваться - person Jonathan Twite; 24.03.2016
comment
Не забывай IReadOnlyCollection<T> - person Dandré; 10.07.2016
comment
Также может быть полезно включить сюда простой массив []. - person jbyrd; 06.12.2019
comment
Хотя это может быть неодобрительно, спасибо, что поделились этим рисунком и статьей - person Daniel; 23.04.2020

Класс, реализующий IEnumerable, позволяет использовать синтаксис foreach.

В основном у него есть способ получить следующий элемент в коллекции. Ему не нужно, чтобы вся коллекция находилась в памяти, и он не знает, сколько в ней элементов, foreach просто продолжает получать следующий элемент, пока он не закончится.

Это может быть очень полезно в определенных обстоятельствах, например, в большой таблице базы данных вы не хотите копировать все в память до того, как вы начнете обрабатывать строки.

Теперь List реализует IEnumerable, но представляет всю коллекцию в памяти. Если у вас есть IEnumerable и вы вызываете .ToList(), вы создаете новый список с содержимым перечисления в памяти.

Ваше выражение linq возвращает перечисление, и по умолчанию выражение выполняется, когда вы выполняете итерацию с использованием foreach. Оператор 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 ) ...
person Keith    schedule 02.09.2010
comment
Но что произойдет, если вы выполните foreach для IEnumerable без предварительного преобразования его в список? Запоминает ли он всю коллекцию? Или он создает экземпляр элемента один за другим, выполняя итерацию цикла foreach? Благодарность - person Pap; 03.04.2016
comment
@Pap последний: он выполняется снова, ничего автоматически не кэшируется в памяти. - person Keith; 03.04.2016
comment
похоже, что ключевой дифференциал: 1) все в памяти или нет. 2) IEnumerable позволяет мне использовать foreach, в то время как список будет идти, скажем, по индексу. Теперь, если я хочу заранее узнать количество / длину thing, IEnumerable не поможет, верно? - person Jeb50; 05.09.2018
comment
@ Jeb50 Не совсем так - и List, и Array реализуют IEnumerable. Вы можете думать о IEnumerable как о наименьшем общем знаменателе, который работает как для коллекций памяти, так и для больших, которые получают по одному элементу за раз. Когда вы вызываете IEnumerable.Count(), вы, возможно, вызываете быстрое свойство .Length или просматриваете всю коллекцию - дело в том, что с IEnumerable вы не знаете. Это может быть проблемой, но если вы просто собираетесь foreach это, тогда вам все равно - ваш код будет работать с Array или DataReader одинаковыми. - person Keith; 05.09.2018
comment
получение следующего элемента, пока он не закончится, означает ли это отправлять запрос в базу данных для каждого элемента? - person M Fuat; 30.11.2018
comment
@MFouadKajj Я не знаю, какой стек вы используете, но он почти наверняка не запрашивает каждую строку. Сервер выполняет запрос и вычисляет начальную точку набора результатов, но не получает всего. Для небольших наборов результатов это, вероятно, будет одна поездка, для больших вы отправляете запрос на дополнительные строки из результатов, но он не запускает повторно весь запрос. - person Keith; 03.12.2018
comment
Значит, использование IEnumerable для миллионов записей отправит миллионы SQL-запросов select в базу данных? - person Shaiju T; 18.08.2020
comment
@shaijut Не должно, но это может зависеть от конкретного провайдера. В Microsoft SQL Server вы получаете клиент курсор, который сохраняет соединение открытым, и клиент просто запрашивает следующую запись в наборе. Это не обходится без затрат, так как это означает, что вам нужно либо новое соединение для параллельного выполнения другого запроса к БД, либо соединение MARS. Слишком много для комментария - person Keith; 27.08.2020

Никто не упомянул об одном существенном различии, иронически ответил на вопрос, закрытый как дубликат этого.

IEnumerable доступен только для чтения, а List - нет.

См. Практическая разница между List и IEnumerable

person CAD bloke    schedule 09.12.2014
comment
Как следствие, это из-за аспекта интерфейса или из-за аспекта списка? т.е. доступен ли IList только для чтения? - person Jason Masters; 26.02.2019
comment
IList не доступен только для чтения - docs.microsoft.com/en-us/dotnet/api/ IEnumerable доступен только для чтения, поскольку в нем отсутствуют какие-либо методы для добавления или удаления чего-либо после создания, это один из базовых интерфейсов. который расширяет IList (см. ссылку) - person CAD bloke; 26.02.2019

Важно понимать, что при использовании Linq запрос не оценивается немедленно. Он запускается только как часть итерации по результирующему IEnumerable<T> в foreach - это то, что делают все странные делегаты.

Итак, первый пример оценивает запрос немедленно, вызывая ToList и помещая результаты запроса в список.
Второй пример возвращает IEnumerable<T>, который содержит всю информацию, необходимую для выполнения запроса позже.

Что касается производительности, ответ - зависит. Если вам нужно, чтобы результаты оценивались сразу (скажем, вы изменяете структуры, которые запрашиваете позже, или если вы не хотите, чтобы итерация по IEnumerable<T> занимала много времени), используйте список. В противном случае используйте IEnumerable<T>. По умолчанию во втором примере следует использовать оценку по требованию, поскольку при этом обычно используется меньше памяти, если нет особой причины для сохранения результатов в списке.

person thecoop    schedule 02.09.2010
comment
Привет и спасибо за ответ :: -). Это развеяло почти все мои сомнения. Любая идея, почему Enumerable разделен на внутренний и внешний? Это происходит, когда я проверяю элемент в режиме отладки / прерывания с помощью мыши. Возможно, это вклад Visual Studio? Перечисление на месте и указание ввода и вывода Enum? - person Axonn; 02.09.2010
comment
Это Join выполняет свою работу - внутренняя и внешняя - две стороны соединения. Как правило, не беспокойтесь о том, что на самом деле находится в IEnumerables, поскольку он будет полностью отличаться от вашего реального кода. Беспокойтесь только о фактическом выходе, когда вы перебираете его :) - person thecoop; 03.09.2010

Преимущество IEnumerable - отложенное выполнение (обычно с базами данных). Запрос не будет выполнен до тех пор, пока вы не переберете данные в цикле. Это запрос, ожидающий, пока он не понадобится (также известный как ленивая загрузка).

Если вы вызовете ToList, запрос будет выполнен или «материализован», как я люблю говорить.

У обоих есть свои плюсы и минусы. Если вы вызовете ToList, вы можете убрать некоторую загадку относительно того, когда будет выполнен запрос. Если вы придерживаетесь IEnumerable, вы получаете то преимущество, что программа не выполняет никакой работы, пока она действительно не понадобится.

person Matt Sherman    schedule 02.09.2010

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

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

person amd    schedule 26.11.2016
comment
Это из-за методов (расширения) linq, которые в этом случае исходят из IEnumerable, где только создают запрос, но не выполняют его (за кулисами используются деревья выражений). Таким образом, у вас есть возможность делать много вещей с этим запросом, не касаясь данных (в данном случае данных в списке). Метод List принимает подготовленный запрос и выполняет его в отношении источника данных. - person Bronek; 18.12.2016
comment
На самом деле, я прочитал все ответы, и я проголосовал за ваш, потому что в нем четко указана разница между ними, не говоря конкретно о LINQ / SQL. Важно знать все это ДО того, как вы перейдете к LINQ / SQL. Восхищаться. - person BeemerGuy; 28.12.2016
comment
Это важное различие, которое нужно объяснить, но на самом деле ваш ожидаемый результат не ожидается. Вы говорите это, как будто это какая-то ошибка, а не дизайн. - person Neme; 22.01.2017
comment
@Neme, да, это было моим ожиданием, прежде чем я понял, как работает IEnumerable, но теперь не больше, так как я знаю, как;) - person amd; 22.01.2017
comment
Хотя это важная концепция для понимания, на самом деле это не дает ответа на вопрос. - person lukkea; 21.02.2021

Если все, что вам нужно сделать, это перечислить их, используйте IEnumerable.

Однако помните, что изменение исходной перечисляемой коллекции является опасной операцией - в этом случае вы сначала захотите ToList. Это создаст новый элемент списка для каждого элемента в памяти с перечислением IEnumerable и, следовательно, будет менее производительным, если вы перечисляете только один раз, но более безопасным, и иногда методы List удобны (например, при произвольном доступе).

person Daren Thomas    schedule 02.09.2010
comment
Я не уверен, что можно с уверенностью сказать, что создание списка означает снижение производительности. - person Steven Sudit; 02.09.2010
comment
@ Стивен: действительно, как сказали thecoop и Крис, иногда может потребоваться использование списка. В моем случае я пришел к выводу, что это не так. @ Дарен: что вы имеете в виду, создавая новый список для каждого элемента в памяти? Может ты имел ввиду запись в списке? :: -). - person Axonn; 02.09.2010
comment
@Axonn да, я помню запись в списке. фиксированный. - person Daren Thomas; 03.09.2010
comment
@Steven Если вы планируете перебирать элементы в IEnumerable, то сначала создание списка (и перебор этого) означает, что вы перебираете элементы дважды. Поэтому, если вы не хотите выполнять более эффективные операции, указанные в списке, это действительно означает более низкую производительность. - person Daren Thomas; 03.09.2010
comment
Предполагая, что мы просто собираемся перебрать все результаты ровно один раз, нет никаких преимуществ в создании списка, если (как вы сказали) операция не выиграет от произвольного доступа. Создание списка всегда стоит чего-то. Я считаю, что, если это LINQ to SQL или если обработка нетривиальна, то кеширование результатов в списке позволяет нам заплатить один раз, а затем перебирать его так часто, как нам нравится, дешево. Поскольку накладные расходы на создание списка довольно низкие, нетрудно найти случаи, когда выгода перевешивает эти затраты. Надеюсь, это объясняет мои мысли. - person Steven Sudit; 03.09.2010
comment
Хуже того, я думаю, мы согласны. - person Steven Sudit; 03.09.2010
comment
@Daren - кроме случаев, когда вас укусили за задницу, когда IEnumerable ведет себя не совсем так, как вы ожидали (и причина, по которой я здесь, в Stack Overflow, исследую его :-)). Я делал for / each поверх XPathSelectElements (), и без добавления .ToList() вызовы подпоследовательности в .Remove() не смогли удалить выбранные XElements. Все еще не совсем уверен, почему это должно происходить именно так - так что вперед читать дальше! - person jerhewet; 04.06.2011
comment
@jerhewet: никогда не стоит изменять повторяемую последовательность. Произойдут плохие вещи. Абстракции потекут. Демоны ворвутся в наше измерение и сеют хаос. Так что да, .ToList() здесь помогает;) - person Daren Thomas; 07.06.2011

В дополнение ко всем ответам, опубликованным выше, вот мои два цента. Есть много других типов, помимо List, которые реализуют IEnumerable, такие как ICollection, ArrayList и т. Д. Итак, если у нас есть IEnumerable в качестве параметра любого метода, мы можем передать функции любые типы коллекций. Т.е. у нас может быть метод для работы с абстракцией, а не с какой-либо конкретной реализацией.

person Ananth    schedule 15.06.2017

Во многих случаях (например, бесконечный список или очень большой список) IEnumerable не может быть преобразован в список. Наиболее очевидными примерами являются все простые числа, все пользователи facebook с их данными или все товары на ebay.

Разница в том, что объекты «Список» хранятся «прямо здесь и прямо сейчас», тогда как объекты «IEnumerable» работают «только по одному». Так что, если я просматриваю все элементы на ebay, по одному будет то, с чем может справиться даже маленький компьютер, но при использовании ".ToList ()" у меня наверняка не хватит памяти, независимо от того, насколько велик мой компьютер. Ни один компьютер не может сам по себе содержать и обрабатывать такое огромное количество данных.

[Edit] - Что и говорить - это не «ни то, ни то». часто имеет смысл использовать и список, и IEnumerable в одном классе. Ни один компьютер в мире не может перечислить все простые числа, потому что по определению для этого потребуется бесконечное количество памяти. Но вы легко можете подумать о class PrimeContainer, который содержит IEnumerable<long> primes, который по очевидным причинам также содержит SortedList<long> _primes. все простые числа, рассчитанные на данный момент. следующее простое число, которое нужно проверить, будет выполняться только против существующих простых чисел (до квадратного корня). Таким образом вы получаете и то, и другое - простые числа по одному (IEnumerable) и хороший список «простых чисел на данный момент», который является довольно хорошим приближением всего (бесконечного) списка.

person LongChalk    schedule 11.03.2019