Когда использовать каждый из T[], List‹T›, IEnumerable‹T›?

Обычно я делаю что-то вроде:

string[] things = arrayReturningMethod();
int index = things.ToList<string>.FindIndex((s) => s.Equals("FOO"));
//do something with index
return things.Distinct(); //which returns an IEnumerable<string>

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

Является ли это идиоматичным и правильным С# или есть лучшая альтернатива, позволяющая избежать обратного и обратного преобразования для доступа к правильным методам работы с данными?

РЕДАКТИРОВАТЬ: Вопрос на самом деле двоякий:

  • Когда правильно использовать либо интерфейс IEnumerable, либо массив, либо список (или любой другой тип реализации IEnumerable) напрямую (при приеме параметров)?

  • Должны ли вы свободно перемещаться между IEnumerables (реализация неизвестна) и списками, IEnumerables, массивами, массивами и списками, или это неидиоматично (есть лучшие способы сделать это)/неэффективно (обычно не актуально, но может быть в некоторых случаях)/ просто уродливые (необслуживаемые, нечитаемые)?


person Vinko Vrsalovic    schedule 04.08.2010    source источник
comment
вы ничего не делаете с индексом :)   -  person µBio    schedule 05.08.2010


Ответы (7)


Что касается производительности...

  • Преобразование из списка в T[] включает в себя копирование всех данных из исходного списка во вновь выделенный массив.
  • Преобразование из T[] в список также включает копирование всех данных из исходного списка во вновь выделенный список.
  • Преобразование из List или T[] в IEnumerable включает приведение типов, что занимает несколько циклов процессора.
  • Преобразование из IEnumerable в List включает в себя повышение приведения, что также требует нескольких циклов процессора.
  • Преобразование из IEnumerable в T[] также включает повышение класса.
  • Вы не можете привести IEnumerable к T[] или List, если это не было T[] или List соответственно для начала. Вы можете использовать функции ToArray или ToList, но они также приведут к созданию копии.
  • Доступ ко всем значениям по порядку от начала до конца в T[] в прямом цикле будет оптимизирован для использования простой арифметики указателей, что делает его самым быстрым из всех.
  • Доступ ко всем значениям по порядку от начала до конца в списке включает проверку на каждой итерации, чтобы убедиться, что вы не получаете доступ к значению за пределами массива, а затем фактический доступ к значению массива.
  • Доступ ко всем значениям в IEnumerable включает создание объекта перечислителя, вызов функции Next(), которая увеличивает указатель индекса, а затем вызов свойства Current, которое дает вам фактическое значение и вставляет его в переменную, указанную в вашем операторе foreach. . В общем, это не так плохо, как кажется.
  • Доступ к произвольному значению в IEnumerable включает в себя запуск с самого начала и вызов Next() столько раз, сколько вам нужно, чтобы добраться до этого значения. Как правило, это так плохо, как кажется.

По поводу идиом...

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

Например, если бы у вас была функция PrintValues, если бы она была написана как PrintValues(List‹T> values), она могла бы иметь дело только со значениями List, поэтому пользователю сначала пришлось бы преобразовать, если, например, они использовали в[]. Аналогично, если функция была PrintValues(T[] values). Но если бы это были PrintValues(значения IEnumerable‹T>), он мог бы иметь дело со списками, T[], стеками, хеш-таблицами, словарями, строками, наборами и т. д. — любой коллекцией, которая реализует IEnumerable, что практически коллекция.

Что касается внутреннего употребления...

  • Используйте список, только если вы не уверены, сколько элементов должно быть в нем.
  • Используйте T[], если вы знаете, сколько элементов должно быть в нем, но вам нужно получить доступ к значениям в произвольном порядке.
  • Придерживайтесь IEnumerable, если это то, что вам дали, и вам просто нужно использовать его последовательно. Многие функции возвращают IEnumerables. Если вам нужно получить доступ к значениям из IEnumerable в произвольном порядке, используйте ToArray().

Кроме того, обратите внимание, что приведение типов отличается от использования ToArray() или ToList() — последнее включает в себя копирование значений, что действительно является ударом по производительности и памяти, если у вас много элементов. Первый просто означает, что «Собака — это животное, поэтому, как и любое другое животное, она может есть» (подавленное) или «Это животное оказалось собакой, поэтому оно может лаять» (подавленное). Точно так же все списки и T[] являются IEnumerables, но только некоторые IEnumerables являются списками или T[]s.

person Rei Miyasaka    schedule 04.08.2010
comment
+1, отличное сравнение. Также во многих случаях - хотя, возможно, за пределами исходного вопроса - интересно использовать IList<T> вместо IEnumerable<T>, особенно когда данные должны быть добавлены и удалены. - person Dirk Vollmar; 05.08.2010

Хорошее эмпирическое правило состоит в том, чтобы всегда использовать IEnumerable (при объявлении ваших переменных/параметров метода/типов/свойств возвращаемого значения метода/и т. д.), если у вас нет веской причины не делать этого. На сегодняшний день наиболее совместим по типам с другими (особенно с расширением) методами.

person Kirk Woll    schedule 04.08.2010
comment
Любой метод расширения, который работает с IEnumerable<T>, также всегда будет работать с List<T> и массивами, поскольку они его реализуют. (Я согласен использовать это всякий раз, когда это возможно, но рассуждение здесь неверно...) - person Reed Copsey; 05.08.2010
comment
Разве FindIndex() не хорошая причина? Может быть, можно было бы реализовать его поверх одного из методов расширения для IEnumerable? - person Vinko Vrsalovic; 05.08.2010
comment
@Vinko: Я бы не стал для этого преобразовывать в список - вы можете сделать это напрямую с помощью Select, а ToList заставляет вас перебирать всю коллекцию и копировать каждый элемент ... - person Reed Copsey; 05.08.2010
comment
IEnumerable — это не тип данных, это интерфейс. Вы не можете использовать IEnumerable, вы можете использовать только вещи, которые реализуют IEnumerable, которых довольно много (строковые массивы и списки, находящиеся в этой категории) - person riwalk; 05.08.2010
comment
Не просто совместимый с другими, но такой мощный. Вы можете связать его, скрыть, отложить загрузку, преобразовать — все это скрыто за одним из самых хорошо продуманных контрактов в .NET :) - person Rex M; 05.08.2010
comment
Если бы было -1/2, я бы сделал это, так как на самом деле это не решает проблему. string[] и List являются IEnumerable, поэтому они даже не отвечают на вопрос. Если бы вы могли отредактировать свой ответ, это был бы лучший вариант. - person riwalk; 05.08.2010
comment
@Stargazer, насколько я понимаю вопрос ОП, он хочет знать, какие типы использовать / объявлять. Если это вопрос, ответ IEnumerable‹T›. Однако я не совсем уверен, в чем на самом деле заключается вопрос ОП. - person Kirk Woll; 05.08.2010
comment
@ Кирк, ему интересно, в чем разница и когда их использовать. Говоря, всегда использовать IEnumerable игнорирует тот факт, что вы не можете объявить экземпляр IEnumerable. Когда вам нужно создать экземпляр объекта, string[] и List‹› оба реализуют IEnumerable, поэтому использование IEnumerable не делает ничего, чтобы отличить их друг от друга. Не поймите меня неправильно — при принятии объекта всегда используйте IEnumerable, но это решает только часть вопроса. - person riwalk; 05.08.2010
comment
Я бы изменил ваш ответ, сказав вместо этого: всегда используйте IEnumerable<T> для общедоступных параметров, возвращаемых типов, свойств и т. д., за исключением случаев, когда вам нужно .Count, и в этом случае используйте IList<T>. Для локальных переменных просто используйте тот тип, который у вас есть. - person Daniel Pryden; 05.08.2010
comment
@Daniel Pryden - что не так с методом расширения Count()? :) .. и когда вам нужно, часто невозможно узнать возвращаемые значения, потому что вы не всегда можете предсказать потребности вызывающего абонента. - person Peter Lillevold; 05.08.2010
comment
@Peter Lillevold: Что не так с методом расширения .Count()? Он имеет производительность O (n) на универсальном IEnumerable<T>, вот что не так. Я пытался сказать, что когда вы пишете функцию, которая требует реализации O(1) .Count и/или случайного доступа к элементам, лучше требовать IList<T>, чем какую-то конкретную реализацию, например List<T> или T[]. Что касается возвращаемого значения, я бы сказал, что нужно возвращать IList<T> только в том случае, если (а) это дешево сделать, и (б) есть разумные ожидания, что вызывающая сторона захочет выполнить произвольный доступ и/или O(1) .Count. - person Daniel Pryden; 05.08.2010

Итак, у вас есть два яблока и апельсин, которые вы сравниваете.

Два яблока — это массив и список.

  • Массив в C# — это массив в стиле C со встроенной сборкой мусора. Преимущество их использования в том, что у них очень мало накладных расходов, при условии, что вам не нужно перемещать вещи. Плохо то, что они не так эффективны, когда вы добавляете что-то, удаляете что-то и иным образом меняете массив, поскольку память перемешивается.

  • Список — это динамический массив в стиле C# (похожий на класс vector‹> в C++). Существует больше накладных расходов, но они более эффективны, когда вам нужно много перемещать вещи, поскольку они не будут пытаться поддерживать непрерывное использование памяти.

Лучшее сравнение, которое я мог бы привести, это сказать, что массивы относятся к спискам так же, как строки к StringBuilder.

Оранжевый — это «IEnumerable». Это не тип данных, а интерфейс. Когда класс реализует интерфейс IEnumerable, он позволяет использовать этот объект в цикле foreach().

Когда вы возвращаете список (как в своем примере), вы не преобразовывали список в IEnumerable. Список уже является объектом IEnumerable.

РЕДАКТИРОВАТЬ: Когда конвертировать между двумя:

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

Надеюсь, это немного рассеет туман.

person riwalk    schedule 04.08.2010
comment
Хорошо, что интерфейс стал другим зверем (+1), хотя на самом деле это не дает ответа на вопрос, когда и что разыгрывать. - person Vinko Vrsalovic; 05.08.2010

Мне кажется, проблема в том, что вы не удосужились научиться искать в массиве. Подсказка: Array.IndexOf или Array.BinarySearch в зависимости от того, отсортирован ли массив.

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

person Ben Voigt    schedule 04.08.2010
comment
Вы не переключаетесь на IEnumerable, это уже является IEnumerable. Это называется полиморфизм... вздох - person riwalk; 05.08.2010
comment
@Vinko: System.Array - это не пространство имен, это класс. @Stargazer: я переключился на upcast. Это делает его более ясным для вас? Существует цена производительности и функциональности полиморфизма. - person Ben Voigt; 05.08.2010
comment
Намного лучше, спасибо. Меня раздражает, что люди относятся к IEquivalent как к тому же, что и List. Это яблоки и апельсины. - person riwalk; 05.08.2010
comment
Под переключением я имел в виду изменение формального типа параметров и возвращаемых значений, но upcast — это гораздо более точное и информативное описание того, что происходит, когда это изменение сделано, так что, надеюсь, никто не будет думать с точки зрения преобразования. - person Ben Voigt; 05.08.2010

Я стараюсь избегать быстрых переходов между типами данных, если этого можно избежать.

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

person kbrimington    schedule 04.08.2010

Когда что использовать?

Я бы предложил вернуть наиболее конкретный тип и использовать наиболее гибкий тип.

Как это:

public int[] DoSomething(IEnumerable<int> inputs)
{
    //...
}

public List<int> DoSomethingElse(IList<int> inputs)
{
    //...
}

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

person Arjan Einbu    schedule 05.08.2010

Вы правы, игнорируя антенны «проблемы с производительностью», пока у вас действительно не возникнет проблема с производительностью. Большинство проблем с производительностью возникает из-за слишком большого количества операций ввода-вывода, слишком большого количества блокировок или неправильного выполнения одного из них, и ни один из этих факторов не относится к данному вопросу.

Мой общий подход таков:

  1. Используйте T[] для «статической» или «моментальной» информации. Используйте для вещей, где вызов .Add() в любом случае не имеет смысла, и вам не нужны дополнительные методы, которые дает вам List‹T>.
  2. Примите IEnumerable‹T>, если вам все равно, что вам дано, и вам не нужен .Length/.Count с постоянным временем.
  3. Возвращайте IEnumerable‹T> только тогда, когда вы выполняете простые манипуляции с вводом IEnumerable‹T> или когда вы специально хотите использовать синтаксис yield для ленивого выполнения своей работы.
  4. Во всех остальных случаях используйте List‹T>. Это слишком гибко.

Следствие № 4: не бойтесь ToList(). ToList() — ваш друг. Это заставляет IEnumerable‹T> вычислять сразу (полезно, когда вы складываете несколько предложений where). Не сходите с ума с этим, но не стесняйтесь вызывать его после того, как вы создали свое полное предложение where, прежде чем делать над ним foreach (или тому подобное).

Конечно, это всего лишь примерный ориентир. Просто, пожалуйста, попробуйте следовать одному и тому же шаблону в той же кодовой базе — стили кода, которые перескакивают с одного места на другое, мешают кодировщикам сопровождения проникнуть в ваше настроение.

person Jonathan Rupp    schedule 04.08.2010
comment
Вы говорите только о веб-разработке. Вы были бы сумасшедшим, если бы постоянно конвертировали в игре или, например, в какой-то алгоритмической работе, или если вы обрабатываете большие списки с миллионами элементов. Список является самым дорогим из трех, а также излишне конкретным. Используйте его, только если вы не знаете, сколько элементов у вас есть, когда вы заполняете массив. Не принимайте списки в качестве аргументов. Если вам нужно получить значения аргумента произвольно, вместо этого примите IList; в противном случае просто примите IEnumerable. В противном случае вы просто просите преломления ада. - person Rei Miyasaka; 05.08.2010