Повернення IList <T> гірше, ніж повернення T [] або List <T>?


80

Відповіді на такі запитання: List <T> або IList <T> завжди схожі на те, що повернення інтерфейсу краще, ніж повернення конкретної реалізації колекції. Але я борюся з цим. Примірник інтерфейсу неможливий, тому якщо ваш метод повертає інтерфейс, він фактично все одно повертає конкретну реалізацію. Я трохи експериментував із цим, написавши 2 невеликі методи:

public static IList<int> ExposeArrayIList()
{
    return new[] { 1, 2, 3 };
}

public static IList<int> ExposeListIList()
{
    return new List<int> { 1, 2, 3 };
}

І використовуйте їх у моїй тестовій програмі:

static void Main(string[] args)
{
    IList<int> arrayIList = ExposeArrayIList();
    IList<int> listIList = ExposeListIList();

    //Will give a runtime error
    arrayIList.Add(10);
    //Runs perfectly
    listIList.Add(10);
}

В обох випадках, коли я намагаюся додати нове значення, мій компілятор не видає мені помилок, але, очевидно, метод, який виставляє мій масив як IList<T>помилку, дає помилку виконання, коли я намагаюся щось додати до нього. Тож люди, які не знають, що відбувається в моєму методі, і їм потрібно додати значення, змушені спочатку скопіювати мій IListу a, Listщоб мати змогу додавати значення без ризику помилок. Звичайно, вони можуть зробити перевірку типу, щоб побачити, чи мають вони справу з a Listабо an Array, але якщо вони цього не роблять, і вони хочуть додати елементи до колекції, у них немає іншого вибору для копіювання в ILista List, навіть якщо це вже єList . Чи не слід масив ніколи виставляти як IList?

Ще одне моє занепокоєння ґрунтується на прийнятій відповіді на пов'язане запитання (наголос на моєму):

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

Якщо ви просто використовуєте його внутрішньо, вам може бути все одно, і використання списку може бути нормальним.

Уявіть, хтось насправді використовував моє, IList<T>отримане від мого ExposeListIlist()методу, просто для додавання / видалення значень. Все працює нормально. Але тепер, як пропонується відповідь, оскільки повернення інтерфейсу є більш гнучким, я повертаю масив замість Списку (з моєї сторони не проблема!), Тоді вони чекають задоволення ...

TLDR:

1) Оголення інтерфейсу спричиняє непотрібні ролі? Це не має значення?

2) Іноді, якщо користувачі бібліотеки не використовують прив'язку, їх код може зламатися, коли ви зміните метод, хоча метод залишається цілком чудовим.

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


9
Потім поверніться, IEnumerable<T>і ви скомпілюєте безпеку часу. Ви все ще можете використовувати всі методи розширення LINQ, які зазвичай намагаються оптимізувати продуктивність, приводячи її до певного типу (наприклад, ICollection<T>використовувати Countвластивість замість перерахування).
Тім Шмельтер

4
Масиви реалізуються IList<T>"хаком". Якщо ви хочете виставити метод, що повертає його, поверніть тип, який насправді його реалізує.
CodeCaster

3
Тільки не давай обіцянок, яких не можеш виконати. Клієнтський програміст, швидше за все, буде розчарований, коли прочитає ваш контракт "Я поверну список". Це не так, просто не робіть цього.
Ганс Пасант,

7
З MSDN : IList є нащадком інтерфейсу ICollection і є базовим інтерфейсом усіх не-загальних списків. Реалізації IList поділяються на три категорії: лише для читання, фіксованого розміру та змінного розміру. IList лише для читання не може бути змінений. IList фіксованого розміру не дозволяє додавати або видаляти елементи, але дозволяє модифікувати існуючі елементи. IList змінного розміру дозволяє додавати, видаляти та модифікувати елементи.
Хамід Пурджам,

3
@AlexanderDerck: якщо ви хочете, щоб споживач міг додати елементи до вашого списку, не робіть це IEnumerable<T>спочатку. Тоді добре повернутися IList<T>або List<T>.
Тім Шмельтер

Відповіді:


108

Можливо, це не відповідає безпосередньо на ваше запитання, але в .NET 4.5+ я вважаю за краще дотримуватися цих правил при розробці загальнодоступних або захищених API:

  • повернути IEnumerable<T>, якщо доступне лише перерахування;
  • повернути, IReadOnlyCollection<T>якщо доступні як перелік, так і кількість елементів;
  • повернути IReadOnlyList<T>, якщо доступні перелік, підрахунок предметів та індексований доступ;
  • повернути, ICollection<T>якщо доступні перелік, підрахунок та модифікація елементів;
  • повернути IList<T>, якщо доступні перелік, кількість елементів, індексований доступ та модифікація.

Останні два варіанти припускають, що цей метод не повинен повертати масив як IList<T>реалізацію.


2
Насправді не відповідь, але хороший емпіричний принцип і дуже корисний для мене :) Останнім часом я дуже боровся з вибором, що повернути. На здоров’я!
Олександр Дерк,

1
Це справді правильний принцип +1. Тільки для повноти я б додав, IReadOnlyCollection<T>і ICollection<T>які вони використовуються для неіндексуваних контейнерів (наприклад, пізніше фактично є стандартом для властивостей навігаційних властивостей підсистеми EF)
Іван Стоєв,

@IvanStoev Чи можете ви відредагувати відповідь, будь ласка, з доданими? Було б непогано, якби я нарешті отримав список, де який інтерфейс використовувати
Олександр Дерк

3
Слід зазначити, що IReadOnlyCollection<T>і IReadOnlyList<T>мають такий же недолік, як і у C ++ const. Є дві можливості: або колекція незмінна, або лише ваш спосіб доступу до колекції забороняє вам її змінювати. І ви не можете відрізнити одне від іншого.
ach

1
@Panzercrisis: Я б обрав другий варіант, звичайно - IEnumerable<T>. Як розробник API ви маєте повні знання про дозволені випадки використання. Якщо результати методу призначені лише для перерахування, тоді немає причин виставляти IList<T>. Чим менше дозволено робити з результатом, тим гнучкішим є впровадження методу. Наприклад, експозиція IEnumerable<T>дозволяє змінити реалізацію на yield returns,IList<T> не дозволяє цього.
Денніс

31

Ні, оскільки споживач повинен знати, що саме таке IList :

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

Ви можете перевірити IList.IsFixedSizeі IList.IsReadOnlyі робити з цим, що хочете.

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

Детальніше читайте, якщо ви хочете прийняти рішення про повернення інтерфейсу


ОНОВЛЕННЯ

Покопавши більше, я виявив, що IList<T>це не реалізується IListі IsReadOnlyдоступне через базовий інтерфейс, ICollection<T>але немає IsFixedSizeдля IList<T>. Детальніше про те, чому загальний IList <> не успадковує не-загальний IList?


8
Ці властивості хороші, але, на мою думку, це все одно перешкоджає призначенню інтерфейсу. Для мене інтерфейс - це контракт, який не повинен залежати від перевірок чи чогось іншого
Олександр Дерк

1
Насправді було б добріше для розробника, якби вони надали додатковий інтерфейс, який має (нефіксований розмір / не для читання) List<T>семантику, але, ймовірно, є вагомі міркування, чому це не було включено .... але, так, я думаю, що ваша ідея кількох інтерфейсів, що охоплюють різні функції, була б набагато обґрунтованішим вибором;
Спендер

1
IList<T>це жирний інтерфейс, але це найбільше, що ви можете отримати від масиву та списку. Отже, якби це розбилося на кілька інтерфейсів, можливо, ви не зможете використовувати деякі методи зі списками та масивами, а лише з одним із них, навіть якщо всі методи, які ви хочете використовувати, підтримуються (наприклад, IList.Countабо індексатор). Тож, на мою думку, це було гарне рішення. Якщо це викликає NotSupportedExceptionдуже рідкісні випадки, коли хтось хоче використовувати Addмасив, то це справа.
Тім Шмельтер

1
@dotctor Який збіг обставин, я вчора читав про принцип заміщення Ліскова, і це мене насправді задумало :) дякую за відповідь
Олександр Дерк

6
Ну, ця відповідь у поєднанні з правилами Денніса, безумовно, може покращити розуміння того, як боротися з цією дилемою. Само собою зрозуміло, що принцип заміщення Ліскова порушується тут і там у .NET framework, і це не щось особливе, про що слід турбуватися.
Фабжан

13

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

Якщо виставити в List<T>якості члена (полів, властивості, методи, ...), ви говорите споживачеві цього члена: типу , отриманий шляхом доступу до цього методу єList<T> , або чим - то похідне від цього.

Тепер, якщо ви виставите інтерфейс, ви приховуєте "деталь реалізації" вашого класу, використовуючи конкретний тип. Звичайно , ви не можете створити екземпляр IList<T>, але ви можете використовувати Collection<T>, List<T>, їх похідні або ваш власний тип реалізації IList<T>.

Фактичний питання є «Чому Arrayреалізувати IList<T>» , або «Чому має IList<T>інтерфейс так багато членів» .

Це також залежить від того, що ви хочете зробити від споживачів цього члена. Якщо ви фактично повертаєте внутрішнього члена через свого Expose...члена, ви все new List<T>(internalMember)одно захочете повернути його , оскільки в іншому випадку споживач може спробувати передати їх IList<T>і змінити вашого внутрішнього члена через це.

Якщо ви просто очікуєте, що споживачі повторять результати, виставте IEnumerable<T>чи IReadOnlyCollection<T>замість цього.


3
"Насправді питання" Чому Array реалізує IList <T> ", або" Чому інтерфейс IList <T> стільки членів ".", Це справді те, з чим я боровся.
Олександр Дерк,

2
@Fabjan - IList<T>має методи додавання та видалення елементів, саме тому для масивів погано реалізовувати. IsReadOnlyВластивість хак затушувати відмінності , коли вона повинна була розділена на дві ( по крайней мере) два інтерфейси.
Lee

@Lee Упс, ти маєш рацію, видаляючи мій неточний коментар
Fabjan

1
@AlexanderDerck це те , що я просив раніше stackoverflow.com/q/5968708/706456
алексей

@oleksii це справді схоже, але я б не називав це дублікатом
Олександр Дерк

8

Будьте обережні з ковдрами цитат, які вирвані з контексту.

Повернення інтерфейсу краще, ніж повернення конкретної реалізації

Ця цитата має сенс лише в тому випадку, якщо вона використовується в контексті принципів SOLID . Існує 5 принципів, але для цілей цієї дискусії ми просто поговоримо про останні 3.

Принцип інверсії залежності

слід “Залежати від абстракцій. Не залежать від конкрементів ".

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

Залежно від інтерфейсів (абстракцій). Не залежать від конкретних реалізацій (конкрементів).

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

Принцип заміщення Ліскова

"Об'єкти в програмі повинні бути замінені екземплярами їх підтипів, не змінюючи правильність цієї програми."

Як ви зазначали, повернення a Array- це явно інша поведінка повернення, List<T>навіть якщо вони обидва реалізують IList<T>. Це, безумовно, є порушенням LSP.

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

Принцип сегрегації інтерфейсу

"Багато клієнтських інтерфейсів краще, ніж один загальний інтерфейс".

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

На жаль, інтерфейси в середовищі .NET (зокрема, ранні версії) не завжди є ідеальними інтерфейсами для конкретного клієнта. Хоча, як зазначив у своїй відповіді @Dennis, у .NET 4.5+ є набагато більше можливостей.


Читання за принципом Ліскова в першу чергу мене збентежилоIList<T>
Олександр Дерк

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

5

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

Дійсні причини використання інтерфейсу можуть бути:

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

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

Можна стверджувати, що 1 і 2 в основному однакові причини. Це два різні сценарії, які зрештою призводять до однакової потреби.

"Це контракт". Якщо договір укладено з вами, а ваша заявка закрита як за функціоналом, так і за часом, часто немає сенсу використовувати інтерфейс.


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

2
Розробити все своє програмне забезпечення (і особливо свої класи та бібліотеки класів) може бути дуже просвітницьким з мисленням: "А якби хтось інший мав використовувати цей клас (бібліотеку)?" і "Що робити, якщо я хочу додати функціональність до конкретного типу, який я хочу повернути?" . Типи BCL можуть бути запечатані, тому ви не можете їх розширювати. Тоді ви застрягли, якщо хочете повернути щось інше, оскільки ваш API обіцяє виставити саме цей тип, на відміну від інтерфейсу. Повернення інтерфейсу майже завжди краще, ніж повернення конкретної імплементації.
CodeCaster

1
Я погоджуюсь (справді), я повинен був додати третю причину: "обмежте функціональність поверненого об'єкта до його суті, щоб пояснити повідомлення абоненту". Я теж повертаю інтерфейси IReadOnlyXxx, коли це можливо, а не повноцінний об'єкт, який використовується для збирання колекції.
Мартін Маат,
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.