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

678

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

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

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

Злой Конвертация Списка через .ToList()?

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

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

Аксонн
источник

Ответы:

742

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

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

List<Animals> Leopards = Feline(AllSpotted()).ToList();
List<Animals> Hyenas = Canine(AllSpotted()).ToList();
Крис Уэнам
источник
11
Я думаю, что они относятся к двум сторонам объединения. Если вы нажмете «SELECT * FROM Animals, JOIN Species ...», тогда внутренняя часть объединения будет Animals, а внешняя часть - Species.
Крис Уэнам
10
Когда я прочитал ответы о: IEnumerable <T> vs IQueryable <T>, я увидел аналогичное объяснение, так что IEnumerable автоматически заставляет среду выполнения использовать LINQ to Objects для запроса коллекции. Так что я запутался между этими 3 типами. stackoverflow.com/questions/2876616/…
Бронек
4
@Bronek Ответ, который вы указали, верный. IEnumerable<T>будет LINQ-To-Objects после первой части, означающей, что все обнаруженные объекты должны быть возвращены для запуска Feline. С другой стороны, an IQuertable<T>позволит уточнить запрос, опуская только пятнистых кошек.
Nate
21
Этот ответ очень обманчив! Комментарий @ Нейта объясняет почему. Если вы используете IEnumerable <T>, фильтр произойдет на стороне клиента, несмотря ни на что.
Ганс
5
Да, AllSpotted () будет запущен дважды. Большая проблема с этим ответом заключается в следующем утверждении: «В приведенном выше примере леопарды и гиены преобразуются в отдельные запросы SQL каждый, а база данных возвращает только соответствующие строки». Это неверно, потому что предложение where вызывается для IEnumerable <>, и он знает только, как перебирать объекты, которые уже поступают из базы данных. Если вы сделали возврат AllSpotted () и параметров Feline () и Canine () в IQueryable, тогда фильтр будет выполняться в SQL, и этот ответ будет иметь смысл.
Ганс
178

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

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

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

rubStackOverflow
источник
25
Следует отметить, что эта статья предназначена только для открытых частей вашего кода, а не для внутренней работы. Listявляется реализация IListи как таковой имеет дополнительные функциональные возможности сверх тех , что в IList(например Sort, Find, InsertRange). Если вы заставите себя использовать IListболее List, вы потеряете эти методы , которые вам может потребоваться
Jonathan Twite
4
Не забывайтеIReadOnlyCollection<T>
Дандре
2
Здесь также может быть полезно включить простой массив [].
jbyrd
Несмотря на то, что это может вызывать недовольство, спасибо за то, что поделились этой графикой и статьей
Даниэль
134

Класс, который реализует 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 без предварительного преобразования его в List ? Приносит ли это целую коллекцию в память? Или он создает элемент один за другим, повторяя цикл foreach? спасибо
Пап
@Pap последний: он выполняется снова, ничего автоматически не кэшируется в памяти.
Кит
Похоже, ключ различий 1) все это в памяти или нет. 2) IEnumerable позволяет мне использовать, foreachпока List будет идти, скажем, по индексу. Теперь, если я хотел бы знать кол / длина от thingзаранее, IEnumerable не поможет, не так ли?
Jeb50
@ Jeb50 Не совсем так - и так Listи Arrayреализуем IEnumerable. Вы можете думать IEnumerableкак о наименьшем общем знаменателе, который работает как для коллекций памяти, так и для больших, которые получают по одному элементу за раз. Когда вы звоните, IEnumerable.Count()вы, возможно, звоните быстро .Lengthили просматриваете всю коллекцию - дело в том, что IEnumerableвы не знаете. Это может быть проблемой, но если вы просто идете к foreachэтому, то вам все равно - ваш код будет работать с Arrayили DataReaderтем же.
Кит
1
@MFouadKajj Я не знаю, какой стек вы используете, но он почти наверняка не делает запрос с каждой строкой. Сервер запускает запрос и вычисляет начальную точку набора результатов, но не получает целиком. Для небольших наборов результатов это может быть одна поездка, для больших вы отправляете запрос на большее количество строк из результатов, но он не перезапускает весь запрос.
Кит
97

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

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

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

CAD CAD
источник
Как следствие, это из-за аспекта интерфейса или из-за списка? т.е. IList тоже только для чтения?
Джейсон Мастерс
IList не только для чтения - docs.microsoft.com/en-us/dotnet/api/… IEnumerable доступен только для чтения, поскольку в нем отсутствуют какие-либо методы для добавления или удаления чего-либо после его создания, это один из базовых интерфейсов, который IList расширяется (см. Ссылку)
CAD CAD
67

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

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

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

thecoop
источник
Привет и спасибо за ответ :: -). Это прояснило почти все мои сомнения. Есть идеи, почему Enumerable «разбивается» на «внутренний» и «внешний»? Это происходит, когда я проверяю элемент в режиме отладки / прерывания с помощью мыши. Возможно, это вклад 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() );

Play Arround

https://repl.it/E8Ki/0

драм
источник
1
Это из-за методов linq (расширение), которые в этом случае происходят из IEnumerable, где только создают запрос, но не выполняют его (за кулисами используются деревья выражений). Таким образом, у вас есть возможность сделать много вещей с этим запросом, не касаясь данных (в данном случае данных в списке). Метод List берет подготовленный запрос и выполняет его по отношению к источнику данных.
Бронек
2
На самом деле, я прочитал все ответы, и именно за вас я проголосовал, потому что в них четко говорится о разнице между ними, не говоря уже о LINQ / SQL. Важно знать все это, ДО того, как вы перейдете к LINQ / SQL. Полюбуйтесь.
BeemerGuy
Это важное различие, которое нужно объяснить, но ваш «ожидаемый результат» на самом деле не ожидается. Вы говорите это, как будто это что-то вроде гоча, а не дизайн.
Неме
@Neme, да это было мое ожидание , прежде чем я понять , как IEnumerableработает, но теперь не больше , так как я знаю , как;)
драмы
15

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

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

Дарен Томас
источник
1
Я не уверен, что можно с уверенностью сказать, что создание списка означает снижение производительности.
Стивен Судит
@ Стивен: действительно, как сказал thecoop и Крис, иногда может быть необходимо использовать список. В моем случае я пришел к выводу, что это не так. @ Дарен: что вы подразумеваете под «это создаст новый список для каждого элемента в памяти»? Может быть, вы имели в виду «запись в списке»? :: -).
Аксонн
@Axonn да, я делаю запись в списке. фиксированный.
Дарен Томас
@Steven Если вы планируете выполнять итерации по элементам в IEnumerable, то сначала создание списка (и повторение по нему) означает, что вы выполняете итерации по элементам дважды . Поэтому, если вы не хотите выполнять операции, которые более эффективны в списке, это действительно означает снижение производительности.
Дарен Томас
3
@jerhewet: никогда не стоит изменять итерируемую последовательность. Будут плохие вещи. Абстракции будут течь. Демоны ворвутся в наше измерение и нанесут ущерб. Так что да, .ToList()помогает здесь;)
Дарен Томас
5

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

Ananth
источник
1

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

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

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

LongChalk
источник