Подтвердить что ты не робот

Объяснение, почему IEnumerable более эффективен, чем список

Я продолжаю слышать, что в .net 3.5 вы должны использовать IEnumerable над списком, но я не могу найти какие-либо справочные материалы или статьи, которые объясняют, почему его гораздо более опытный. Кто-нибудь знает какой-либо контент, который объясняет это?

Цель этого вопроса - лучше понять, что делает IEnumerable под капотом. Если вы можете предоставить мне какие-либо ссылки, я сделаю исследование и отправлю ответ.

4b9b3361

Ответ 1

IEnumerable<T> - это интерфейс, который реализуется List<T>. Я подозреваю, что причина, по которой вы слышите, что IEnumerable<T> должна использоваться, заключается в том, что она требует менее жесткого интерфейса.

Например, рассмотрим следующую сигнатуру метода:

void Output(List<Foo> foos) 
{ 
    foreach(var foo in foos) { /* do something */ }
}

Этот метод требует передачи конкретной реализации списка. Но это просто делает что-то в порядке. На самом деле не нужен произвольный доступ или любые другие вещи, которые дает List<T> или даже IList<T>. Вместо этого метод должен принимать IEnumerable<T>:

void Output(IEnumerable<Foo> foos) 
{ 
    foreach(var foo in foos) { /* do something */ }
}

Теперь мы используем самый общий (наименее специфический) интерфейс, который поддерживает те операции, которые нам нужны. Это фундаментальный аспект OO-дизайна. Мы уменьшили сцепление, требуя только того, что нам нужно, а не совсем другого. Мы также создали более гибкий метод, поскольку параметр foos может быть Queue<T>, a List<T>, все, что реализует IEnumerable<T>. Мы не заставляем вызывающего устройства без необходимости переводить свою структуру данных в список.

Таким образом, это не то, что IEnumerable<T> более эффективен, чем список в аспекте "производительность" или "время выполнения". Это то, что IEnumerable<T> является более эффективной конструкцией, потому что это более конкретное указание на то, что требует ваш дизайн. (Хотя это может привести к увеличению времени выполнения в определенных случаях.)

Ответ 2

В перечислениях есть несколько очень хороших свойств, которые вы теряете при преобразовании их в список. А именно:

  • Использовать отложенное/ленивое выполнение
  • Составляемые
  • Без ограничений

Сначала я посмотрю на отсроченное исполнение. Pop quiz: сколько раз будет повторяться следующий код по строкам во входном файле?

IEnumerable<string> ReadLines(string fileName)
{
    using (var rdr = new StreamReader(fileName) )
    {
       string line;
       while ( (line = rdr.ReadLine()) != null) yield return line;
    }
}


var SearchIDs = new int[] {1234,4321, 9802};

var lines = ReadLines("SomeFile.txt")
              .Where(l => l.Length > 10 && l.StartsWith("ID: "));
              .Select(l => int.Parse(l.Substring(4).Trim()));
              .Intersect(SearchIDs);

Ответ - это ровно one ноль. На самом деле это не работает, пока вы не перейдете к результатам. Вам нужно добавить этот код, прежде чем он даже откроет файл:

foreach (string line in lines) Console.WriteLine(line);

Даже после того, как код запускается, он по-прежнему только один раз проецирует строки. Сравните это с тем, сколько раз вам нужно перебирать строки в этом коде:

var SearchIDs = new int[] {1234,4321, 9802};
var lines = File.ReadAllLines("SomeFile.txt"); //creates a list
lines = lines.Where(l => l.Length > 10 && l.StartsWith("ID: ")).ToList();
var ids = lines.Select(l => int.Parse(l.Substring(4).Trim())).ToList();
ids = ids.Intersect(SearchIDs).ToList();

foreach (string line in lines) Console.WriteLine(line);

Даже если вы проигнорируете вызов File.ReadAllLines() и используете тот же блок-итератор из первого образца, первый образец все равно будет быстрее. Конечно, вы могли бы написать это так же быстро, используя списки, но для этого требуется привязать код, который читает файл, коду кода, который анализирует его. И поэтому вы теряете еще одну важную особенность: композитивность.

Чтобы продемонстрировать композитоспособность, я добавлю одну из последних функций — неограниченная серия. Рассмотрим следующее:

IEnumerable<int> Fibonacci()
{
   int n1 = 1, n2 = 0, n;
   yield return 1;
   while (true)
   {
        n = n1 + n2;
        yield return n;
        n2 = n1;
        n1 = n;
   }
}

Похоже, что это будет продолжаться вечно, но вы можете использовать свойства слияния IEnumerable для создания чего-то, что безопасно дает, скажем, первые 50 значений или каждое значение, которое меньше заданного числа:

  foreach (int f in Fibonacci().Take(50)) { /* ... */ }
  foreach (int f in Fibonacci().TakeWhile(i => i < 1000000) { /* ... */ }

Наконец, IEnumerable является более гибким. Если вам абсолютно не нужна возможность добавления в список или доступа к элементам по индексу, вы почти всегда лучше записываете функции, чтобы принимать IEnumerables в качестве аргументов вместо списков. Зачем? Потому что вы все равно можете передать список функции, если вы хотите — Список - это IEnumerable. В этом отношении, так же как и массив, и многие другие типы коллекций хорошо. Таким образом, используя здесь IEnumerable, вы берете ту же самую функцию и делаете ее более мощной, потому что она может работать с более разными видами данных.

Ответ 3

IEnumerable<T> не эффективнее, чем a List<T>, поскольку List<T> является IEnumerable<T>.

Интерфейс IEnumerable<T> - это просто способ .NET использовать итератор, не более того.

Этот интерфейс может быть реализован на многих типах (List<T>в комплекте), чтобы эти типы могли возвращать итераторы (например, экземпляры IEnumerator<T>), чтобы вызывающий мог выполнять итерацию последовательности элементов.

Ответ 4

Это не вопрос эффективности (хотя это может быть правдой), но и гибкости.

Ваш код становится более пригодным для повторного использования, если он может использовать IEnumerable вместо List. AS для эффективного рассмотрения этого кода: -

 function IEnumerable<int> GetDigits()
 {

    for(int i = 0; i < 10; i++)
       yield return i
 }

 function int Sum(List<int> numbers)
 {
    int result = 0; 
    foreach(int i in numbers)
      result += i;

    return i;
 }

Q. Как взять набор чисел, сгенерированный GetDigits, и получить Sum для их добавления?
A: мне нужно загрузить набор чисел из GetDigits в объект List и передать его функции Sum. Это использует память, так как все цифры должны быть загружены в память прежде, чем их можно будет суммировать. Однако изменение сигнатуры Sum на: -

 function int Sum(IEnumerable<int> numbers)

Значит, я могу это сделать: -

 int sumOfDigits = Sum(GetDigits());

В память не загружен список. Мне нужна только память для текущей цифры и суммарной суммы аккумулятора.

Ответ 5

Это два разных зверя, и вы не можете их сравнить. Например, в var q = from x in ... q есть IEnumerable, но под капотом он выполняет очень дорогой вызов базы данных.

An IEnumerable является просто интерфейсом для шаблона проектирования Iterator, тогда как List/IList является контейнером данных.

Ответ 6

Одна из причин, по которой рекомендуется возвращать методы IEnumerable<T>, заключается в том, что она менее специфична, чем List<T>. Это означает, что вы можете впоследствии изменить внутренности вашего метода, чтобы использовать что-то, что может быть более эффективным для его нужд, и пока это IEnumerable<T> вам не нужно прикасаться к контракту вашего метода.

Ответ 7

В .NET 3.5 использование IEnumerable позволяет вам писать методы с отложенным исполнением, например следующие:

public class MyClass
{
   private List<int> _listOne;
   private List<int> _listTwo;
public IEnumerable<int> GetItems () { foreach (int n in _listOne) { yield return n; } foreach (int n in _listTwo) { yield return n; } } }
код >

Это позволяет объединить два списка без создания нового объекта List<int>.