Список <T> или IList <T> [закрыто]


495

Может кто-нибудь объяснить мне, почему я хотел бы использовать IList над списком в C #?

Смежный вопрос: почему считается плохим разоблачатьList<T>


1
Поищите в Интернете «код интерфейса, а не реализацию», и вы можете читать весь день .. и найти несколько достойных войн.
granadaCoder

Ответы:


446

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

Если вы просто используете его для внутреннего использования, вам может быть все равно, и использование List<T>может быть нормальным.


19
Я не понимаю (тонкую) разницу, есть какой-нибудь пример, на который вы могли бы мне указать?
StingyJack

134
Скажем, вы изначально использовали List <T> и хотели изменить на использование специализированного CaseInsensitiveList <T>, оба из которых реализуют IList <T>. Если вы используете конкретный тип, все абоненты должны быть обновлены. Если выставлено как IList <T>, вызывающая сторона не должна быть изменена.
tvanfosson

46
Ах. ХОРОШО. Я понимаю. Выбор наименьшего общего знаменателя для контракта. Спасибо!
StingyJack

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

14
Также обратите внимание, что T []: IList <T>, так что по соображениям производительности вы всегда можете поменять местами возвращение List <T> из вашей процедуры на возврат T [], и если объявлена ​​процедура для возврата IList <T> (или, возможно, ICollection <T> или IEnumerable <T>), больше ничего не нужно будет менять.
yfeldblum

322

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

Архитектура космонавтов . Скорее всего, вы когда-нибудь напишете свой собственный IList, который добавит что-либо к тем, что уже есть в .NET Framework, настолько мал, что это теоретические желе, зарезервированные для «лучших практик».

Программное обеспечение космонавтов

Очевидно, что если вас спрашивают, что вы используете в интервью, вы говорите IList, улыбнитесь, и оба выглядят довольными собой за то, что они такие умные. Или для публичного API, IList. Надеюсь, вы поняли мою точку зрения.


65
Я должен не согласиться с вашим сардоническим ответом. Я не полностью не согласен с чувством чрезмерной архитектуры, являющейся реальной проблемой. Однако я думаю, что особенно в случае коллекций, интерфейсы действительно сияют. Скажем, у меня есть функция, которая возвращает IEnumerable<string>внутри функции, которую я могу использовать List<string>для внутреннего резервного хранилища, чтобы сгенерировать мою коллекцию, но я только хочу, чтобы вызывающие абоненты перечисляли ее содержимое, а не добавляли или удаляли. Принятие интерфейса в качестве параметра приводит к аналогичному сообщению «Мне нужна коллекция строк, но не беспокойтесь, я не буду ее менять».
Джошперри

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

48
Кто-то должен был сказать это Ареку ... даже если у вас есть все виды академической картины, преследующей ненавистническую почту в результате. +1 для всех нас, кто ненавидит это, когда небольшое приложение загружается интерфейсами, и нажатие «найти определение» приводит нас куда-то ДРУГОЕ, чем источник проблемы ... Могу ли я позаимствовать фразу «Архитектура Астронавтов»? Я вижу, это пригодится.
Гатс

6
Если это будет возможно, я дам вам 1000 баллов за этот ответ. : D
Самуил

8
Я говорил о более широком результате поиска паттернов. Интерфейсы для общих интерфейсов хорошие, дополнительные слои ради погони за шаблоном, плохие.
Гатс

186

Интерфейс - это обещание (или контракт).

Как всегда с обещаниями - чем меньше, тем лучше .


56
Не всегда лучше, просто легче держать! :)
Кирк Бродхерст

5
Я не понимаю этого. Так IListи обещание. Что здесь меньше и лучше? Это не логично.
nawfal

3
Для любого другого класса я бы согласился. Но не для List<T>, потому что AddRangeне определяется на интерфейсе.
Джесси де Вит

3
Это не очень полезно в данном конкретном случае. Лучшее обещание - это то, что сдержано. IList<T>дает обещания, которые он не может сдержать. Например, есть Addметод, который может выдать, если он доступен только IList<T>для чтения. List<T>с другой стороны, всегда доступен для записи и, следовательно, всегда выполняет свои обещания. Кроме того, начиная с .NET 4.6, у нас теперь есть IReadOnlyCollection<T>и IReadOnlyList<T>которые почти всегда лучше, чем IList<T>. И они ковариантны. Избегайте IList<T>!
ZunTzu

@ZunTzu Я бы сказал, что кто-то, выдавший неизменную коллекцию за изменяемую, сознательно нарушил представленный контракт - например, это не проблема самого контракта IList<T>. Кто-то может также реализовать IReadOnlyList<T>и заставить каждый метод также выдавать исключение. Это как бы противоположность набиранию уток - вы называете это уткой, но она не может крякать как одна. Это часть контракта, даже если компилятор не может обеспечить его выполнение. Я на 100% согласен, что IReadOnlyList<T>или IReadOnlyCollection<T>должен использоваться для доступа только для чтения, хотя.
Шелби Олдфилд

93

Некоторые люди говорят «всегда используйте IList<T>вместо List<T>».
Они хотят, чтобы вы изменили свои сигнатуры метода с void Foo(List<T> input)на void Foo(IList<T> input).

Эти люди не правы.

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

Как незначительный контраргумент, вы можете обнаружить, что каждый звонящий нуждается в List<T>любом случае, и код вызова усеян.ToList()

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

Предположим, у вас есть этот метод:

public Foo(List<int> a)
{
    a.Add(someNumber);
}

Полезный коллега «рефакторинг» метод, чтобы принять IList<int>.

Ваш код теперь не работает, потому что int[]реализует IList<int>, но имеет фиксированный размер. Контракт для ICollection<T>(база IList<T>) требует кода, который использует его для проверки IsReadOnlyфлага, прежде чем пытаться добавить или удалить элементы из коллекции. Контракт на List<T>не имеет.

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

Такое ощущение, что это нарушает принцип подстановки Лискова.

 int[] array = new[] {1, 2, 3};
 IList<int> ilist = array;

 ilist.Add(4); // throws System.NotSupportedException
 ilist.Insert(0, 0); // throws System.NotSupportedException
 ilist.Remove(3); // throws System.NotSupportedException
 ilist.RemoveAt(0); // throws System.NotSupportedException

Но это не так. Ответ на этот вопрос заключается в том, что в примере использовался IList <T> / ICollection <T> неправильно. Если вы используете ICollection <T>, вам нужно проверить флаг IsReadOnly.

if (!ilist.IsReadOnly)
{
   ilist.Add(4);
   ilist.Insert(0, 0); 
   ilist.Remove(3);
   ilist.RemoveAt(0);
}
else
{
   // what were you planning to do if you were given a read only list anyway?
}

Если кто-то передает вам массив или список, ваш код будет работать нормально, если вы каждый раз проверяете флаг и у вас есть запасной вариант ... Но на самом деле; кто так делает? Не знаете заранее, нужен ли вашему методу список, который может принимать дополнительных членов; Вы не указываете это в сигнатуре метода? Что именно вы собираетесь делать, если вам передают список только для чтения, как int[]?

Вы можете подставить List<T>в код, который использует IList<T>/ ICollection<T> правильно . Вы не можете гарантировать, что сможете заменить IList<T>/ ICollection<T>в код, который использует List<T>.

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

Для меня IList<T>ICollection<T>) это худшая часть .NET Framework. IsReadOnlyнарушает принцип наименьшего удивления. Класс, такой как Array, который никогда не позволяет добавлять, вставлять или удалять элементы, не должен реализовывать интерфейс с методами Add, Insert и Remove. (см. также /software/306105/implementing-an-interface-when-you-dont-need-one-of-the-properties )

Является ли IList<T>хорошо подходит для вашей организации? Если коллега попросит вас изменить подпись метода для использования IList<T>вместо List<T>, спросите их, как они добавят элемент в IList<T>. Если они не знают о IsReadOnly(а большинство людей не знают ), то не используйте IList<T>. Когда-либо.


Обратите внимание, что флаг IsReadOnly происходит от ICollection <T> и указывает, могут ли элементы быть добавлены или удалены из коллекции; но просто чтобы действительно запутать вещи, это не указывает, могут ли они быть заменены, что может быть в случае массивов (которые возвращают IsReadOnlys == true).

Для больше на IsReadOnly, см. Определение msoln ICollection <T> .IsReadOnly


5
Отлично, отличный ответ. Я хотел бы добавить, что, даже если IsReadOnlyэто trueдля IList, вы все еще можете изменить его текущие члены! Может быть, это свойство должно быть названо IsFixedSize?
xofz

(это было проверено с передачей a Arrayкак a IList, поэтому я мог изменить текущих членов)
xofz

1
@SamPearson было свойство IsFixedSize для IList в .NET 1, не включенное в универсальный IList <T> .NET 2.0, где оно было объединено с IsReadOnly, что привело к удивительному поведению вокруг массивов. Здесь есть подробный блог о незаметных различиях: enterprisecraftsmanship.com/2014/11/22/…
перфекционист

7
Отличный ответ! Кроме того, начиная с .NET 4.6, у нас теперь есть IReadOnlyCollection<T>и IReadOnlyList<T>которые почти всегда лучше подходят, чем, IList<T>но не имеют опасной ленивой семантики IEnumerable<T>. И они ковариантны. Избегайте IList<T>!
ZunTzu

37

List<T>это конкретная реализация IList<T>, которая является контейнером, который может быть адресован так же, как линейный массив T[]с использованием целочисленного индекса. Когда вы указываете IList<T>в качестве типа аргумента метода, вы указываете только то, что вам нужны определенные возможности контейнера.

Например, спецификация интерфейса не предписывает использование определенной структуры данных. Реализация List<T>происходит с той же производительностью для доступа, удаления и добавления элементов как линейный массив. Однако вы могли бы представить реализацию, которая вместо этого поддерживается связанным списком, для которого добавление элементов в конец дешевле (постоянное время), но произвольный доступ намного дороже. (Обратите внимание , что .NET LinkedList<T>никак не реализовать IList<T>.)

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

Кроме того, вы можете рассмотреть возможность предоставления минимальной функциональности, которая вам нужна. Например. если вам не нужно изменять содержимое списка, вы, вероятно, должны рассмотреть возможность использования IEnumerable<T>, которое IList<T>расширяет.


Что касается вашего последнего заявления об использовании IEnumerable вместо IList. Это не всегда рекомендуется, возьмем, к примеру, WPF, который просто создаст объект-оболочку IList, поэтому он будет иметь эффект перфекта
msdn.microsoft.com/en-us/library/bb613546.aspx

1
Отличный ответ, гарантии производительности - это то, что интерфейс не может предоставить. В некотором коде это может быть очень важно, и использование конкретных классов сообщает ваше намерение, вашу потребность в этом конкретном классе. Интерфейс с другой стороны говорит: «Мне просто нужно вызвать этот набор методов, никаких других контрактов».
Джошперри

1
@joshperry Если производительность интерфейса вас беспокоит, вы в первую очередь не на той платформе. Импо, это микрооптимизация.
nawfal

@nawfal Я не комментировал плюсы и минусы производительности интерфейсов. Я согласился и прокомментировал тот факт, что, когда вы решите представить конкретный класс в качестве аргумента, вы сможете сообщить о намерениях производительности. Взятие LinkedList<T>против взятия List<T>против IList<T>всех сообщает что-то о гарантиях производительности, требуемых вызываемым кодом.
Joshperry

28

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


1
Очень интересный способ думать об этом. И хороший способ думать при программировании - спасибо.
Арахис

Хорошо сказано! Поскольку интерфейс является более гибким подходом, вам действительно нужно оправдать то, что конкретная реализация покупает вам.
Кори Дом

9
Отличный способ мышления. Тем не менее я могу ответить на него: моя причина называется AddRange ()
КПП

4
моя причина называется Add (). иногда вам нужен список, который, как вы знаете, может содержать дополнительные элементы? Смотри мой ответ.
перфекционист

19

IList <T> является интерфейсом, поэтому вы можете наследовать другой класс и при этом реализовать IList <T>, тогда как наследование List <T> не позволяет вам сделать это.

Например, если есть класс A и ваш класс B наследует его, вы не можете использовать List <T>

class A : B, IList<T> { ... }

15

Принцип TDD и ООП обычно заключается в программировании интерфейса, а не реализации.

В этом конкретном случае, поскольку вы по сути говорите о языковой конструкции, а не о пользовательской конструкции, это обычно не имеет значения, но, например, скажем, что вы обнаружили, что List не поддерживает то, что вам нужно. Если бы вы использовали IList в остальной части приложения, вы могли бы расширить List своим собственным классом и по-прежнему иметь возможность передавать его без рефакторинга.

Затраты на это минимальны, почему бы не избавить себя от головной боли позже? В этом и заключается принцип интерфейса.


3
Если ваш ExtendedList <T> наследует List <T>, то вы все равно можете поместить его в контракт List <T>. Этот аргумент работает, только если вы пишете свою собственную реализацию IList <T> из sratch (или по крайней мере без наследования List <T>)
PPC

14
public void Foo(IList<Bar> list)
{
     // Do Something with the list here.
}

В этом случае вы можете передать любой класс, который реализует интерфейс IList <Bar>. Если бы вы использовали взамен List <Bar>, можно было передать только экземпляр List <Bar>.

Способ IList <Bar> более слабо связан, чем способ List <Bar>.


13

Подчеркивая, что ни в одном из этих вопросов (или ответов) относительно Списка или IList не упоминается разница подписей. (Вот почему я искал этот вопрос на SO!)

Итак, вот методы, содержащиеся в List, которых нет в IList, по крайней мере, начиная с .NET 4.5 (около 2015 г.)

  • AddRange
  • AsReadOnly
  • BinarySearch
  • Вместимость
  • ConvertAll
  • Существует
  • найти
  • Найти все
  • FindIndex
  • FindLast
  • FindLastIndex
  • Для каждого
  • GetRange
  • InsertRange
  • LastIndexOf
  • Убрать все
  • RemoveRange
  • Обеспечить регресс
  • Сортировать
  • ToArray
  • TrimExcess
  • TrueForAll

1
Не совсем верно. Reverseи ToArrayявляются методами расширения для интерфейса IEnumerable <T>, из которого происходит IList <T>.
Тарек

Это потому, что они имеют смысл только в Lists, а не в других реализациях IList.
HankCa

12

Наиболее важный случай использования интерфейсов над реализациями - это параметры вашего API. Если ваш API принимает параметр List, то любой, кто его использует, должен использовать List. Если тип параметра - IList, то вызывающая сторона обладает гораздо большей свободой и может использовать классы, о которых вы никогда не слышали, которых, возможно, даже не существовало на момент написания вашего кода.


7

Что делать, если .NET 5.0 заменяет System.Collections.Generic.List<T>на System.Collection.Generics.LinearList<T>. .NET всегда владеет именем, List<T>но они гарантируют, чтоIList<T> это контракт. Таким образом, ИМХО, мы (по крайней мере, я) не должны использовать чье-то имя (хотя в данном случае это .NET) и попадаем в неприятности позже.

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


7

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

IList<T> defines those methods (not including extension methods)

IList<T> Ссылка MSDN

  1. Добавить
  2. Очистить
  3. Содержит
  4. Скопировать в
  5. GetEnumerator
  6. Индекс
  7. Вставить
  8. Удалить
  9. RemoveAt

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

List<T> Ссылка MSDN


4

Вы бы, потому что определение IList или ICollection открыло бы для других реализаций ваших интерфейсов.

Возможно, вы захотите иметь IOrderRepository, который определяет коллекцию заказов в IList или ICollection. Тогда у вас могут быть различные виды реализаций для предоставления списка заказов, если они соответствуют «правилам», определенным вашим IList или ICollection.


3

IList <> почти всегда предпочтителен согласно совету другого автора, однако обратите внимание, что в .NET 3.5 sp 1 есть ошибка при запуске IList <> через более одного цикла сериализации / десериализации с WCF DataContractSerializer.

Теперь есть SP, чтобы исправить эту ошибку: KB 971030


2

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

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


2

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

Кроме того, имея возможность делать что-то вроде изменения стандартной реализации List <T> в классе реализующий IList <T>, например .Add, .Remove или любой другой метод IList, дает разработчику большую гибкость и мощь, в противном случае предопределенные по списку <T>


0

Как правило, хорошим подходом является использование IList в общедоступном API-интерфейсе (когда это необходимо, и семантика списка необходима), а затем внутренний список для реализации API. Это позволяет вам перейти на другую реализацию IList, не нарушая код, который использует ваш класс.

Имя класса List может быть изменено в следующей среде .net, но интерфейс никогда не изменится, так как интерфейс является контрактным.

Обратите внимание, что если ваш API будет использоваться только в циклах foreach и т. Д., Вы можете вместо этого рассмотреть возможность предоставления IEnumerable вместо этого.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.