Может кто-нибудь объяснить мне, почему я хотел бы использовать IList над списком в C #?
Смежный вопрос: почему считается плохим разоблачатьList<T>
Может кто-нибудь объяснить мне, почему я хотел бы использовать IList над списком в C #?
Смежный вопрос: почему считается плохим разоблачатьList<T>
Ответы:
Если вы представляете свой класс через библиотеку, которую будут использовать другие, вы, как правило, хотите представить его через интерфейсы, а не через конкретные реализации. Это поможет, если вы решите изменить реализацию вашего класса позже, чтобы использовать другой конкретный класс. В этом случае пользователям вашей библиотеки не нужно будет обновлять свой код, поскольку интерфейс не меняется.
Если вы просто используете его для внутреннего использования, вам может быть все равно, и использование List<T>
может быть нормальным.
Менее популярный ответ - программисты любят притворяться, что их программное обеспечение будет повторно использоваться во всем мире, когда большинство проектов будут обслуживаться небольшим количеством людей, и как бы ни были хороши звуковые фрагменты, связанные с интерфейсом, вы обманываете сами.
Архитектура космонавтов . Скорее всего, вы когда-нибудь напишете свой собственный IList, который добавит что-либо к тем, что уже есть в .NET Framework, настолько мал, что это теоретические желе, зарезервированные для «лучших практик».
Очевидно, что если вас спрашивают, что вы используете в интервью, вы говорите IList, улыбнитесь, и оба выглядят довольными собой за то, что они такие умные. Или для публичного API, IList. Надеюсь, вы поняли мою точку зрения.
IEnumerable<string>
внутри функции, которую я могу использовать List<string>
для внутреннего резервного хранилища, чтобы сгенерировать мою коллекцию, но я только хочу, чтобы вызывающие абоненты перечисляли ее содержимое, а не добавляли или удаляли. Принятие интерфейса в качестве параметра приводит к аналогичному сообщению «Мне нужна коллекция строк, но не беспокойтесь, я не буду ее менять».
Интерфейс - это обещание (или контракт).
Как всегда с обещаниями - чем меньше, тем лучше .
IList
и обещание. Что здесь меньше и лучше? Это не логично.
List<T>
, потому что AddRange
не определяется на интерфейсе.
IList<T>
дает обещания, которые он не может сдержать. Например, есть Add
метод, который может выдать, если он доступен только IList<T>
для чтения. List<T>
с другой стороны, всегда доступен для записи и, следовательно, всегда выполняет свои обещания. Кроме того, начиная с .NET 4.6, у нас теперь есть IReadOnlyCollection<T>
и IReadOnlyList<T>
которые почти всегда лучше, чем IList<T>
. И они ковариантны. Избегайте IList<T>
!
IList<T>
. Кто-то может также реализовать IReadOnlyList<T>
и заставить каждый метод также выдавать исключение. Это как бы противоположность набиранию уток - вы называете это уткой, но она не может крякать как одна. Это часть контракта, даже если компилятор не может обеспечить его выполнение. Я на 100% согласен, что IReadOnlyList<T>
или IReadOnlyCollection<T>
должен использоваться для доступа только для чтения, хотя.
Некоторые люди говорят «всегда используйте 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
IsReadOnly
это true
для IList
, вы все еще можете изменить его текущие члены! Может быть, это свойство должно быть названо IsFixedSize
?
Array
как a IList
, поэтому я мог изменить текущих членов)
IReadOnlyCollection<T>
и IReadOnlyList<T>
которые почти всегда лучше подходят, чем, IList<T>
но не имеют опасной ленивой семантики IEnumerable<T>
. И они ковариантны. Избегайте IList<T>
!
List<T>
это конкретная реализация IList<T>
, которая является контейнером, который может быть адресован так же, как линейный массив T[]
с использованием целочисленного индекса. Когда вы указываете IList<T>
в качестве типа аргумента метода, вы указываете только то, что вам нужны определенные возможности контейнера.
Например, спецификация интерфейса не предписывает использование определенной структуры данных. Реализация List<T>
происходит с той же производительностью для доступа, удаления и добавления элементов как линейный массив. Однако вы могли бы представить реализацию, которая вместо этого поддерживается связанным списком, для которого добавление элементов в конец дешевле (постоянное время), но произвольный доступ намного дороже. (Обратите внимание , что .NET LinkedList<T>
никак не реализовать IList<T>
.)
В этом примере также говорится, что могут быть ситуации, когда вам нужно указать реализацию, а не интерфейс, в списке аргументов: в этом примере, когда вам требуется конкретная характеристика производительности доступа. Обычно это гарантируется для конкретной реализации контейнера ( List<T>
документация: «Он реализует IList<T>
общий интерфейс, используя массив, размер которого динамически увеличивается по мере необходимости».).
Кроме того, вы можете рассмотреть возможность предоставления минимальной функциональности, которая вам нужна. Например. если вам не нужно изменять содержимое списка, вы, вероятно, должны рассмотреть возможность использования IEnumerable<T>
, которое IList<T>
расширяет.
LinkedList<T>
против взятия List<T>
против IList<T>
всех сообщает что-то о гарантиях производительности, требуемых вызываемым кодом.
Я бы немного перевернул вопрос, вместо того, чтобы обосновать, почему вы должны использовать интерфейс поверх конкретной реализации, попытаться обосновать, почему вы будете использовать конкретную реализацию, а не интерфейс. Если вы не можете оправдать это, используйте интерфейс.
IList <T> является интерфейсом, поэтому вы можете наследовать другой класс и при этом реализовать IList <T>, тогда как наследование List <T> не позволяет вам сделать это.
Например, если есть класс A и ваш класс B наследует его, вы не можете использовать List <T>
class A : B, IList<T> { ... }
Принцип TDD и ООП обычно заключается в программировании интерфейса, а не реализации.
В этом конкретном случае, поскольку вы по сути говорите о языковой конструкции, а не о пользовательской конструкции, это обычно не имеет значения, но, например, скажем, что вы обнаружили, что List не поддерживает то, что вам нужно. Если бы вы использовали IList в остальной части приложения, вы могли бы расширить List своим собственным классом и по-прежнему иметь возможность передавать его без рефакторинга.
Затраты на это минимальны, почему бы не избавить себя от головной боли позже? В этом и заключается принцип интерфейса.
public void Foo(IList<Bar> list)
{
// Do Something with the list here.
}
В этом случае вы можете передать любой класс, который реализует интерфейс IList <Bar>. Если бы вы использовали взамен List <Bar>, можно было передать только экземпляр List <Bar>.
Способ IList <Bar> более слабо связан, чем способ List <Bar>.
Подчеркивая, что ни в одном из этих вопросов (или ответов) относительно Списка или IList не упоминается разница подписей. (Вот почему я искал этот вопрос на SO!)
Итак, вот методы, содержащиеся в List, которых нет в IList, по крайней мере, начиная с .NET 4.5 (около 2015 г.)
Reverse
и ToArray
являются методами расширения для интерфейса IEnumerable <T>, из которого происходит IList <T>.
List
s, а не в других реализациях IList
.
Наиболее важный случай использования интерфейсов над реализациями - это параметры вашего API. Если ваш API принимает параметр List, то любой, кто его использует, должен использовать List. Если тип параметра - IList, то вызывающая сторона обладает гораздо большей свободой и может использовать классы, о которых вы никогда не слышали, которых, возможно, даже не существовало на момент написания вашего кода.
Что делать, если .NET 5.0 заменяет System.Collections.Generic.List<T>
на System.Collection.Generics.LinearList<T>
. .NET всегда владеет именем, List<T>
но они гарантируют, чтоIList<T>
это контракт. Таким образом, ИМХО, мы (по крайней мере, я) не должны использовать чье-то имя (хотя в данном случае это .NET) и попадаем в неприятности позже.
В случае использования IList<T>
вызывающая сторона всегда гарантирует работу вещей, и разработчик может изменить базовую коллекцию на любую альтернативную конкретную реализациюIList
Все концепции в основном изложены в большинстве ответов выше относительно того, зачем использовать интерфейс поверх конкретных реализаций.
IList<T> defines those methods (not including extension methods)
IList<T>
Ссылка MSDN
List<T>
реализует эти девять методов (не включая методы расширения), кроме того, он имеет около 41 общедоступных методов, что зависит от того, какой из них использовать в вашем приложении.
List<T>
Ссылка MSDN
Вы бы, потому что определение IList или ICollection открыло бы для других реализаций ваших интерфейсов.
Возможно, вы захотите иметь IOrderRepository, который определяет коллекцию заказов в IList или ICollection. Тогда у вас могут быть различные виды реализаций для предоставления списка заказов, если они соответствуют «правилам», определенным вашим IList или ICollection.
IList <> почти всегда предпочтителен согласно совету другого автора, однако обратите внимание, что в .NET 3.5 sp 1 есть ошибка при запуске IList <> через более одного цикла сериализации / десериализации с WCF DataContractSerializer.
Теперь есть SP, чтобы исправить эту ошибку: KB 971030
Интерфейс гарантирует, что вы по крайней мере получите методы, которые вы ожидаете ; быть в курсе определения интерфейса т.е. все абстрактные методы, которые должны быть реализованы любым классом, наследующим интерфейс. поэтому, если кто-то создает собственный огромный класс с несколькими методами, помимо тех, которые он унаследовал от интерфейса для некоторой дополнительной функциональности, и которые вам бесполезны, лучше использовать ссылку на подкласс (в этом случае интерфейс) и назначить ему конкретный объект класса.
дополнительное преимущество заключается в том, что ваш код защищен от любых изменений в конкретном классе, так как вы подписываетесь только на несколько методов конкретного класса, и это те, которые будут там, пока конкретный класс наследует от интерфейса, которым вы являетесь с помощью. так что это безопасность для вас и свобода для кодировщика, который пишет конкретную реализацию, чтобы изменить или добавить больше функциональности в его конкретный класс.
Вы можете взглянуть на этот аргумент с нескольких точек зрения, в том числе с точки зрения чисто ОО-подхода, который говорит, что программирование на основе интерфейса не является реализацией. С этой мыслью использование IList следует тому же принципу, что и обход и использование интерфейсов, которые вы определяете с нуля. Я также верю в факторы масштабируемости и гибкости, предоставляемые интерфейсом в целом. Если класс, реализующий IList <T>, необходимо расширить или изменить, код потребления не должен изменяться; он знает, чего придерживается контракт IList Interface. Однако использование конкретной реализации и List <T> для класса, который изменяется, также может привести к необходимости изменения вызывающего кода. Это потому, что класс, придерживающийся IList <T>, гарантирует определенное поведение, которое не гарантируется конкретным типом, использующим List <T>.
Кроме того, имея возможность делать что-то вроде изменения стандартной реализации List <T> в классе реализующий IList <T>, например .Add, .Remove или любой другой метод IList, дает разработчику большую гибкость и мощь, в противном случае предопределенные по списку <T>
Как правило, хорошим подходом является использование IList в общедоступном API-интерфейсе (когда это необходимо, и семантика списка необходима), а затем внутренний список для реализации API. Это позволяет вам перейти на другую реализацию IList, не нарушая код, который использует ваш класс.
Имя класса List может быть изменено в следующей среде .net, но интерфейс никогда не изменится, так как интерфейс является контрактным.
Обратите внимание, что если ваш API будет использоваться только в циклах foreach и т. Д., Вы можете вместо этого рассмотреть возможность предоставления IEnumerable вместо этого.