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

или

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

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

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

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

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

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

Ответ 1

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

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

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

Ответ 3

Класс, реализующий 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 ) ...

Ответ 5

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

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

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

Ответ 6

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

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

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

Ответ 7

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

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

Ответ 8

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

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

Ответ 9

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

Ответ 10

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

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

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