Может кто-нибудь объяснить мне, почему я хотел бы использовать IList over List в С#?
Связанный с этим вопрос: Почему это плохо, чтобы разоблачить List<T>
Может кто-нибудь объяснить мне, почему я хотел бы использовать IList over List в С#?
Связанный с этим вопрос: Почему это плохо, чтобы разоблачить List<T>
Если вы публикуете свой класс через библиотеку, которую другие будут использовать, вы, как правило, хотите разоблачить ее через интерфейсы, а не конкретные реализации. Это поможет, если вы решите изменить реализацию своего класса позже, чтобы использовать другой конкретный класс. В этом случае пользователям вашей библиотеки не потребуется обновлять свой код, так как интерфейс не изменяется.
Если вы просто используете его внутренне, вам может быть не так много, и использование List<T>
может быть в порядке.
Менее популярный ответ заключается в том, что программисты любят притворяться, что их программное обеспечение будет повторно использоваться во всем мире, когда большинство проектов будет поддерживаться небольшим количеством людей, и, как бы ни были хороши связанные с интерфейсом звуковые звуки, вы "обманывать себя".
Архитектура астронавтов. Вероятность того, что вы когда-нибудь напишите свой собственный IList, который добавляет что-либо к тем, которые уже существуют в платформе .NET, настолько далеки, что теоретические новички-маркеры зарезервированы для "лучших практик".
Очевидно, если вас спросят, что вы используете в интервью, вы говорите, IList, улыбайтесь, и оба выглядят доволен собой за то, что вы так умны. Или для публичного API, IList. Надеюсь, вы поймете мою мысль.
Интерфейс - это обещание (или контракт).
Как всегда с promises - меньше, тем лучше.
Некоторые люди говорят "всегда используйте 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>
.
Обращение к принципу единой ответственности/принципу разделения интерфейсов во многих аргументах для использования абстракций вместо конкретных типов - зависит от самого узкого интерфейса. В большинстве случаев, если вы используете List<T>
и думаете, что вместо этого можете использовать более узкий интерфейс - почему бы не IEnumerable<T>
? Это часто лучше подходит, если вам не нужно добавлять элементы. Если вам нужно добавить в коллекцию, используйте конкретный тип, List<T>
.
Для меня IList<T>
(и ICollection<T>
) - худшая часть .NET Framework. IsReadOnly
нарушает принцип наименьшего удивления. Класс, такой как Array
, который никогда не позволяет добавлять, вставлять или удалять элементы, не должен реализовывать интерфейс с методами Add, Insert и Remove. (см. также https://softwareengineering.stackexchange.com/questions/306105/implementing-an-interface-when-you-dont-need-one-of-the-properties)
IList<T>
ли IList<T>
для вашей организации? Если коллега попросит вас изменить подпись метода, чтобы использовать IList<T>
вместо List<T>
, спросите их, как они добавят элемент в IList<T>
. Если они не знают об IsReadOnly
(а большинство людей этого не знают), не используйте IList<T>
. Когда-либо.
Обратите внимание, что флаг IsReadOnly происходит из ICollection <T> и указывает, могут ли элементы быть добавлены или удалены из коллекции; но просто чтобы действительно запутать вещи, это не указывает, могут ли они быть заменены, что в случае массивов (которые возвращают IsReadOnlys == true) может быть.
Для больше на IsReadOnly, см. Определение msoln ICollection <T>.IsReadOnly
List<T>
представляет собой конкретную реализацию IList<T>
, которая представляет собой контейнер, который может быть адресован тем же способом, что и линейный массив T[]
с использованием целочисленного индекса. Когда вы указываете IList<T>
как тип аргумента метода, вы указываете только, что вам нужны определенные возможности контейнера.
Например, спецификация интерфейса не применяет конкретную структуру данных, которая будет использоваться. Реализация List<T>
происходит с одинаковой производительностью для доступа, удаления и добавления элементов в виде линейного массива. Однако вы могли бы представить себе реализацию, которая подкрепляется связанным списком, для которого добавление элементов в конец дешевле (постоянное время), но случайный доступ намного дороже. (Обратите внимание, что .NET LinkedList<T>
не реализует IList<T>
.)
В этом примере также указывается, что могут возникнуть ситуации, когда вам нужно указать реализацию, а не интерфейс, в списке аргументов: В этом примере всякий раз, когда вам требуется конкретная характеристика производительности доступа. Обычно это гарантируется для конкретной реализации контейнера (List<T>
documentation: "Он реализует общий интерфейс IList<T>
, используя массив, размер которого динамически увеличивается по мере необходимости".).
Кроме того, вам может потребоваться рассмотреть возможность использования наименьшей функциональности. Например. если вам не нужно изменять содержимое списка, вам, вероятно, следует рассмотреть возможность использования IEnumerable<T>
, который IList<T>
продолжается.
Я бы поставил вопрос немного, вместо того, чтобы обосновать, почему вы должны использовать интерфейс над конкретной реализацией, попробуйте обосновать, почему вы должны использовать конкретную реализацию, а не интерфейс. Если вы не можете это оправдать, используйте интерфейс.
IList < Т > является интерфейсом, поэтому вы можете наследовать другой класс и все еще осуществлять 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.
Самый важный случай использования интерфейсов над реализациями - это параметры вашего API. Если ваш API принимает параметр List, то каждый, кто его использует, должен использовать List. Если тип параметра ILIST, то вызывающий имеет гораздо большую свободу и может использовать классы, о которых вы никогда не слышали, которые, возможно, даже не существовали при написании вашего кода.
Помните, что ни один из этих ответов List или IList (или ответов) не упоминает разницу подписи. (Вот почему я искал этот вопрос на SO!)
Итак, здесь методы, содержащиеся в List, которые не найдены в IList, по крайней мере, на .NET 4.5 (около 2015 года)
Что делать, если .NET 5.0 заменяет System.Collections.Generic.List<T>
на System.Collection.Generics.LinearList<T>
..NET всегда принадлежит имя List<T>
, но они гарантируют, что IList<T>
является контрактом. Поэтому IMHO мы (по крайней мере, я) не должны использовать имя кого-то (хотя в этом случае это .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
Вы можете посмотреть на этот аргумент с нескольких ракурсов, включая подход, основанный исключительно на OO, который говорит, что программа против интерфейса не является реализацией. С этой мыслью, используя IList, следует тот же принцип, что и прохождение и использование интерфейсов, которые вы определяете с нуля. Я также верю в факторы масштабируемости и гибкости, предоставляемые интерфейсом в целом. Если класс, прикладывающий IList <T> необходимо расширить или изменить, потребительский код не должен изменяться; он знает, к чему согласуется интерфейс IList Interface. Однако, используя конкретную реализацию и список <T> в классе, который изменяется, может привести к необходимости изменения кода вызова. Это связано с тем, что класс, придерживающийся IList <T> гарантирует определенное поведение, которое не гарантируется конкретным типом, используя List <T> .
Также имеет силу сделать что-то вроде изменения реализации по умолчанию List <T> на классе Реализация IList <T> например .Add,.Remove или любой другой IList-метод дает разработчику большую гибкость и мощность, в противном случае предопределенные List <T>
Как правило, хорошим подходом является использование IList в общедоступном API-интерфейсе (когда это необходимо, и семантика списка необходима), а затем внутренний список для реализации API. Это позволяет вам перейти на другую реализацию IList, не нарушая код, который использует ваш класс.
Имя класса List может быть изменено в следующей платформе .net, но интерфейс никогда не изменится, так как интерфейс является контрактным.
Обратите внимание, что если ваш API будет использоваться только в циклах foreach и т.д., Вы можете рассмотреть возможность использования IEnumerable вместо этого.