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


495

Хтось може мені пояснити, чому я хотів би використовувати IList over List у 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>, так що з міркувань продуктивності ви завжди можете змінити обмін списком <T> зі своєї процедури на повернення T [], і якщо процедура оголошена поверненням IList <T> (або, можливо, ICollection <T> або IEnumerable <T>) нічого іншого не потрібно було б змінювати.
yfeldblum

322

Менш популярною відповіддю є програмісти, які хочуть робити вигляд, що їх програмне забезпечення буде використане у всьому світі, коли більшість проектів буде підтримуватися незначною кількістю людей, але як би приємні звукозаписи, пов'язані з інтерфейсом, ви вводите в оману. себе.

Архітектура космонавтів . Шанси, що ви коли-небудь напишіть свій власний IList, який додасть що-небудь до тих, що вже є в .NET-рамках, настільки віддалені, що це теоретичні желейні тоти, зарезервовані для "найкращих практик".

Програмне забезпечення космонавтів

Очевидно, якщо вас запитують, які ви використовуєте в інтерв'ю, ви говорите ІЛІСТ, посміхаєтесь, і обидва дивляться на себе за те, що ви такі розумні. Або для API для громадськості, IList. Сподіваюся, ви зрозуміли мою думку.


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

38
Розробка програмного забезпечення стосується перекладу намірів. Якщо ви вважаєте, що інтерфейси корисні лише для створення великих розмірів, грандіозних архітектур і не мають місця в невеликих магазинах, то я сподіваюся, що людина, що сидить навпроти вас в інтерв'ю, це не я.
joshperry

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-бази. 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


5
Чудова, чудова відповідь. Я хотів би додати, що, навіть якщо IsReadOnlyце trueдля IList, ви все одно можете змінювати його нинішніх членів! Можливо, цю власність слід назвати IsFixedSize?
xofz

(це було тестовано на передачу ArrayIList
знака

1
@SamPearson в ILNET було встановлено властивість IsFixedSize в .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, щоб мати вплив perf
msdn.microsoft.com/en-us/library/bb613546.aspx

1
Чудова відповідь, гарантії продуктивності - це те, що інтерфейс не може забезпечити У деякому коді це може бути дуже важливо, і використання конкретних класів передає ваш намір, вашу потребу в цьому конкретному класі. Інтерфейс з іншого боку говорить: "Мені просто потрібно називати цей набір методів, ніякого іншого контракту не мається на увазі".
joshperry

1
@joshperry Якщо продуктивність інтерфейсу турбує вас, ви в першу чергу на неправильній платформі. Імпо, це мікрооптимізація.
nawfal

@nawfal Я не коментував плюси / мінуси продуктивності інтерфейсів. Я погоджувався і коментував той факт, що коли ви вирішите виставити конкретний клас як аргумент, ви зможете повідомити про наміри виконання. Беручи LinkedList<T>проти прийому List<T>vs IList<T>усі повідомляють щось про гарантії ефективності, що вимагаються кодом, що викликається.
joshperry

28

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


1
Дуже цікавий спосіб її думати. І хороший спосіб думати при програмуванні - дякую.
Арахіс

Добре сказано! Оскільки інтерфейс є більш гнучким підходом, вам дійсно слід виправдати те, що конкретна реалізація купує у вас.
Cory House

9
Чудовий спосіб мислення. Я все ще можу відповісти на це: моя причина називається AddRange ()
PPC

4
моя причина називається Add (). Іноді ви хочете, щоб список, який, на вашу думку , може містити додаткові елементи, безумовно? Дивіться мою відповідь.
перфекціоніст

19

IList <T> - це інтерфейс, завдяки чому ви можете успадкувати інший клас і все ще реалізувати IList <T>, а спадкове Список <T> заважає вам це робити.

Наприклад, якщо є клас A і ваш клас B успадковує його, ви не можете використовувати Список <T>

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

15

Принцип TDD і OOP загалом - це програмування на інтерфейс, а не реалізація.

У цьому конкретному випадку, оскільки ви по суті говорите про мовну конструкцію, а не про власну, вона, як правило, не має значення, але, скажімо, наприклад, що Ви знайшли List, що не підтримує щось необхідне. Якщо ви використовували IList в іншій програмі, ви можете розширити список за допомогою власного користувальницького класу і все-таки зможете пропустити цю програму без рефакторингу.

Вартість цього зробити мінімальна, чому б потім не врятувати собі головний біль пізніше? Це те, в чому полягає принцип інтерфейсу.


3
Якщо ваш ExtendedList <T> успадковує Список <T>, то ви все одно можете вписати його у договір "Список <T>. Цей аргумент працює лише в тому випадку, якщо ви пишете власну реалізацію IList <T> з sratch (або принаймні без спадкового списку <T>)
PPC

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

У цьому випадку ви можете перейти до будь-якого класу, який реалізує інтерфейс IList <Bar>. Якщо ви використовували List <Bar> замість цього, може бути переданий лише екземпляр List <Bar>.

Шлях IList <Bar> є більш вільно пов'язаним, ніж шлях Список <Bar>.


13

Не дивлячись на те, що жоден із цих списків щодо списку проти IList (або відповідей) не згадує про різницю підписів. (Ось чому я шукав це питання на ТАК!)

Ось ось методи, що містяться у списку, які не зустрічаються в IList, принаймні станом на .NET 4.5 (близько 2015 року)

  • AddRange
  • AsReadOnly
  • BinarySearch
  • Ємність
  • ConvertAll
  • Існує
  • Знайдіть
  • FindAll
  • 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>це договір. Таким чином, IMHO ми (принаймні I) не повинні використовувати чиєсь ім’я (хоча це .NET в цьому випадку) і пізніше потраплятимемо в проблеми.

У разі використання IList<T>абонент завжди працює з налаштованими програмами, і реалізатор може змінити базову колекцію на будь-яку альтернативну конкретну реалізаціюIList


7

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

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

IList<T> MSDN-посилання

  1. Додайте
  2. Ясно
  3. Містить
  4. CopyTo
  5. GetEnumerator
  6. IndexOf
  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

Ви можете дивитися на цей аргумент з декількох ракурсів, включаючи підхід із суто OO, який говорить про програмування на Інтерфейс, а не на реалізацію. З цією думкою використання IList слідує тому самому принципу, що й обхід та використання інтерфейсів, які ви визначаєте з нуля. Я також вірю в фактори масштабованості та гнучкості, які надає Інтерфейс загалом. Якщо клас імліментації IList <T> потрібно розширити або змінити, споживчий код не повинен змінюватися; він знає, чого дотримується контракт IList Interface. Однак використання конкретної реалізації та List <T> для класу, який змінюється, може також викликати необхідність зміни коду виклику. Це тому, що клас, який дотримується IList <T>, гарантує певну поведінку, яка не гарантована конкретним типом, використовуючи List <T>.

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


0

Як правило, хорошим підходом є використання IList у вашому відкритому API (коли потрібно, і семантика списку), а потім перелічити внутрішньо для впровадження API. Це дозволяє перейти на іншу реалізацію IList, не порушуючи код, який використовує ваш клас.

Список назв класу може бути змінений у наступній .net рамках, але інтерфейс ніколи не буде змінюватися, оскільки інтерфейс є контрактом.

Зауважте, що якщо ваш API буде використовуватися лише в циклах foreach тощо, ви можете замість цього просто розкрити IEnumerable.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.