Хтось може мені пояснити, чому я хотів би використовувати IList over List у C #?
Супутнє запитання: Чому вважається поганим викриватиList<T>
Хтось може мені пояснити, чому я хотів би використовувати IList over List у C #?
Супутнє запитання: Чому вважається поганим викриватиList<T>
Відповіді:
Якщо ви відкриваєте свій клас через бібліотеку, якою користуватимуться інші, ви, як правило, хочете розкрити його через інтерфейси, а не конкретні реалізації. Це допоможе, якщо ви вирішите пізніше змінити реалізацію свого класу, щоб використовувати інший конкретний клас. У такому випадку користувачам вашої бібліотеки не потрібно буде оновлювати свій код, оскільки інтерфейс не змінюється.
Якщо ви просто використовуєте його всередині, ви можете не так сильно піклуватися, а використання List<T>
може бути нормальним.
Менш популярною відповіддю є програмісти, які хочуть робити вигляд, що їх програмне забезпечення буде використане у всьому світі, коли більшість проектів буде підтримуватися незначною кількістю людей, але як би приємні звукозаписи, пов'язані з інтерфейсом, ви вводите в оману. себе.
Архітектура космонавтів . Шанси, що ви коли-небудь напишіть свій власний IList, який додасть що-небудь до тих, що вже є в .NET-рамках, настільки віддалені, що це теоретичні желейні тоти, зарезервовані для "найкращих практик".
Очевидно, якщо вас запитують, які ви використовуєте в інтерв'ю, ви говорите ІЛІСТ, посміхаєтесь, і обидва дивляться на себе за те, що ви такі розумні. Або для 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-бази. IsReadOnly
порушує принцип найменшого здивування. Клас, такий як Array
, який ніколи не дозволяє додавати, вставляти чи видаляти елементи, не повинен реалізовувати інтерфейс із методами Додати, Вставити та Видалити. (див. також /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 див. У msdn визначенні ICollection <T> .IsReadOnly
IsReadOnly
це true
для IList
, ви все одно можете змінювати його нинішніх членів! Можливо, цю власність слід назвати IsFixedSize
?
Array
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>
vs IList<T>
усі повідомляють щось про гарантії ефективності, що вимагаються кодом, що викликається.
Я б трохи обернув питання, замість того, щоб пояснювати, чому слід використовувати інтерфейс над конкретною реалізацією, спробувати виправдати, чому б ви використовували конкретну реалізацію, а не інтерфейс. Якщо ви не можете це виправдати, використовуйте інтерфейс.
Принцип TDD і OOP загалом - це програмування на інтерфейс, а не реалізація.
У цьому конкретному випадку, оскільки ви по суті говорите про мовну конструкцію, а не про власну, вона, як правило, не має значення, але, скажімо, наприклад, що Ви знайшли List, що не підтримує щось необхідне. Якщо ви використовували IList в іншій програмі, ви можете розширити список за допомогою власного користувальницького класу і все-таки зможете пропустити цю програму без рефакторингу.
Вартість цього зробити мінімальна, чому б потім не врятувати собі головний біль пізніше? Це те, в чому полягає принцип інтерфейсу.
public void Foo(IList<Bar> list)
{
// Do Something with the list here.
}
У цьому випадку ви можете перейти до будь-якого класу, який реалізує інтерфейс IList <Bar>. Якщо ви використовували List <Bar> замість цього, може бути переданий лише екземпляр List <Bar>.
Шлях IList <Bar> є більш вільно пов'язаним, ніж шлях Список <Bar>.
Не дивлячись на те, що жоден із цих списків щодо списку проти IList (або відповідей) не згадує про різницю підписів. (Ось чому я шукав це питання на ТАК!)
Ось ось методи, що містяться у списку, які не зустрічаються в 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>
це договір. Таким чином, IMHO ми (принаймні I) не повинні використовувати чиєсь ім’я (хоча це .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. Однак використання конкретної реалізації та List <T> для класу, який змінюється, може також викликати необхідність зміни коду виклику. Це тому, що клас, який дотримується IList <T>, гарантує певну поведінку, яка не гарантована конкретним типом, використовуючи List <T>.
Також, маючи можливість зробити щось на зразок змінити реалізацію за замовчуванням списку <T> для класу, що реалізує IList <T>, наприклад, .Add, .Remove або будь-який інший метод IList, дає розробнику багато гнучкості та потужності, інакше заздалегідь визначені за списком <T>
Як правило, хорошим підходом є використання IList у вашому відкритому API (коли потрібно, і семантика списку), а потім перелічити внутрішньо для впровадження API. Це дозволяє перейти на іншу реалізацію IList, не порушуючи код, який використовує ваш клас.
Список назв класу може бути змінений у наступній .net рамках, але інтерфейс ніколи не буде змінюватися, оскільки інтерфейс є контрактом.
Зауважте, що якщо ваш API буде використовуватися лише в циклах foreach тощо, ви можете замість цього просто розкрити IEnumerable.