Навіщо використовувати IList або List?


82

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

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

Скажіть, чи є у мене такий метод

  public class SomeClass
    {
        public bool IsChecked { get; set; }
    }

 public void LogAllChecked(IList<SomeClass> someClasses)
    {
        foreach (var s in someClasses)
        {
            if (s.IsChecked)
            {
                // log 
            }
        }
    }

Я не впевнений, як використання IList допоможе мені в майбутньому.

Як щодо того, якщо я вже використовую метод? Чи повинен я все ще використовувати IList?

public void LogAllChecked(IList<SomeClass> someClasses)
    {
        //why not List<string> myStrings = new List<string>()
        IList<string> myStrings = new List<string>();

        foreach (var s in someClasses)
        {
            if (s.IsChecked)
            {
                myStrings.Add(s.IsChecked.ToString());
            }
        }
    }

Що я отримую за використання IList зараз?

public IList<int> onlySomeInts(IList<int> myInts)
    {
        IList<int> store = new List<int>();
        foreach (var i in myInts)
        {
            if (i % 2 == 0)
            {
                store.Add(i);
            }
        }

        return store;
    }

Як зараз? Чи є якась нова реалізація списку int, які мені потрібно буде змінити?

В основному, мені потрібно побачити деякі фактичні приклади коду того, як використання IList вирішило б якусь проблему, аніж просто взяв List у все.

З мого прочитання я думаю, що я міг використовувати IEnumberable замість IList, оскільки я просто переглядаю речі.

Редагувати Отже, я бавився з деякими своїми методами, як це зробити. Я все ще не впевнений у типі повернення (якщо я повинен зробити це більш конкретним чи інтерфейсом).

 public class CardFrmVm
    {
        public IList<TravelFeaturesVm> TravelFeaturesVm { get; set; }
        public IList<WarrantyFeaturesVm> WarrantyFeaturesVm { get; set; }

        public CardFrmVm()
        {
            WarrantyFeaturesVm = new List<WarrantyFeaturesVm>();
            TravelFeaturesVm = new List<TravelFeaturesVm>();
        }
}

 public class WarrantyFeaturesVm : AvailableFeatureVm
    {
    }

 public class TravelFeaturesVm : AvailableFeatureVm
    {
    }

 public class AvailableFeatureVm
    {
        public Guid FeatureId { get; set; }
        public bool HasFeature { get; set; }
        public string Name { get; set; }
    }


        private IList<AvailableFeature> FillAvailableFeatures(IEnumerable<AvailableFeatureVm> avaliableFeaturesVm)
        {
            List<AvailableFeature> availableFeatures = new List<AvailableFeature>();
            foreach (var f in avaliableFeaturesVm)
            {
                if (f.HasFeature)
                {
                                                    // nhibernate call to Load<>()
                    AvailableFeature availableFeature = featureService.LoadAvaliableFeatureById(f.FeatureId);
                    availableFeatures.Add(availableFeature);
                }
            }

            return availableFeatures;
        }

Зараз я повертаю IList за простий факт, що потім я додаю це до своєї моделі домену, що має таку властивість:

public virtual IList<AvailableFeature> AvailableFeatures { get; set; }

Вищезазначений сам IList, оскільки це, здається, стандарт для використання з ніхірнатам. Інакше я міг би повернути IEnumberable назад, але не впевнений. Проте я не можу зрозуміти, що потрібно на 100% користувачеві (саме тут повернення бетону має перевагу перед).

Редагувати 2

Я також думав, що трапиться, якщо я хочу передати посилання у своєму методі?

private void FillAvailableFeatures(IEnumerable<AvailableFeatureVm> avaliableFeaturesVm, IList<AvailableFeature> toFill)
            {

                foreach (var f in avaliableFeaturesVm)
                {
                    if (f.HasFeature)
                    {
                                                        // nhibernate call to Load<>()
                        AvailableFeature availableFeature = featureService.LoadAvaliableFeatureById(f.FeatureId);
                        toFill.Add(availableFeature);
                    }
                }
            }

я б не зіткнувся з цим? Оскільки вони не можуть передавати масив (який має фіксований розмір)? Чи було б краще, можливо, для конкретного Переліку?

Відповіді:


156

Тут є три запитання: який тип я повинен використовувати для формального параметра? Що слід використовувати для локальної змінної? і що я повинен використовувати для типу повернення?

Формальні параметри:

Принцип тут - не просити більше, ніж потрібно . IEnumerable<T>повідомляє "Мені потрібно отримати елементи цієї послідовності від початку до кінця". IList<T>повідомляє "Мені потрібно отримати та встановити елементи цієї послідовності у довільному порядку". List<T>повідомляє "Мені потрібно отримати та встановити елементи цієї послідовності у довільному порядку, і я приймаю лише списки; я не приймаю масиви."

Просячи більше, ніж потрібно, ви (1) змушуєте абонента виконувати непотрібну роботу, щоб задовольнити ваші непотрібні вимоги, і (2) повідомляєте читачеві неправди. Запитуйте лише те, що ви збираєтеся використовувати. Таким чином, якщо абонент має послідовність, їм не потрібно викликати ToList, щоб задовольнити ваші запити.

Локальні змінні:

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

Тип повернення:

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


9
Звідки ви знаєте, що потрібно абоненту. Наприклад, я перемикав один із моїх типів повернення на IList <>, тоді я, мабуть, просто збираюся перерахувати їх у будь-якому випадку, дозволяє просто повернути IEnumberable. Потім я подивився на свій погляд (mvc) і виявив, що мені насправді потрібен метод count, як мені потрібно було використовувати цикл for. Тож у власному додатку я підрахував, що мені насправді потрібно, як ви передбачаєте, що комусь іншому буде потрібно чи ні.
chobo2

@ Chobo2: У вашому конкретному прикладі, LINQ Countпрацює в O (1) , якщо ваш IEnumerableє ICollection. Звичайно, ви також можете просто використовувати foreachцикл.
Брайан

6
@ chobo2: Ну, як ти передбачаєш, які методи потрібні абоненту? Здається, проблему вирішити першим. Імовірно, якось у вас є спосіб дізнатись, які методи писати для людей, які збираються їх викликати. Запитайте цих людей, що вони хотіли б, щоб методи повернули. Ваше питання принципово "як я знаю, яке програмне забезпечення писати?" Ви знаєте, дізнавшись, які проблеми має вирішити ваш клієнт, і написавши код, який вирішує їх проблеми.
Ерік Ліпперт,

1
@Eric - вам слід оновити свою відповідь щодо формальних параметрів, включивши приклад ICollection, де вам потрібно лише додати елемент. Також у вашому поясненні типу повернення повинно бути щось на зразок "пропонувати лише мінімальний мінімум того, що ви дозволяєте виконувати абоненту". Якщо його список лише для читання повертає лише IEnumerable тощо
Чарльз Ламберт

4
@kvb: Майже. Розглянемо ваш перший сценарій. Ви можете зробити алгоритмічне вдосконалення, виконуючи дії, items as IList<T>і якщо ви отримаєте ненульове значення, тоді скористайтеся своїм вдосконаленим кодом. Таким чином ви користуєтеся перевагами, якщо можете, дозволяючи клієнту гнучкість у тому, що вони передають.
Ерік Ліпперт,

33

Найбільш практичну причину, яку я коли-небудь бачив, дав Джеффрі Ріхтер у CLR через C #.

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

Наприклад, наступний метод

public void PrintTypes(IEnumerable items) 
{ 
    foreach(var item in items) 
        Console.WriteLine(item.GetType().FullName); 
}

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

public void PrintTypes(List items)

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

Говорячи про типи повернення, чим конкретніші ви, тим гнучкішими можуть бути користувачі, які телефонують.

public List<string> GetNames()

Ви можете використовувати цей тип повернення для ітерації імен

foreach(var name in GetNames())

або ви можете індексувати безпосередньо в колекції

Console.WriteLine(GetNames()[0])

Тоді як, якщо ви поверталися менш конкретний тип

public IEnumerable GetNames()

вам доведеться масажувати тип повернення, щоб отримати перше значення

Console.WriteLine(GetNames().OfType<string>().First());

10
Зауважте, що ця порада суперечить відповіді Еріка Ліпперта в рекомендації щодо типів повернення. Підхід Джеффрі Ріхтера надає споживачам методу найбільшу гнучкість у використанні поверненого об'єкта, як їм подобається, тоді як Ерік надає супровідникам методу найбільшу гнучкість для зміни реалізації без зміни публічної поверхні методу. Я схильний слідувати порадам Джеффрі щодо внутрішнього кодексу, але для публічної бібліотеки я, мабуть, був би більш схильний дотримуватися рекомендацій Еріка.
phoog

3
@phoog: Беручи до уваги, звідки Ерік, не дивно, що він більш обережно ставиться до порушення змін. Але це, безумовно, вагомий пункт.

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

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

1
+1 Дрібниці: ця відповідь є приємним, специфічним для CLR прикладом закону Постеля . :)
Dan J

13

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

Якщо ви виставляєте своє майно як List<T>або навіть IList<T>тоді, коли все, що ви хочете, щоб у вашого споживача, - це можливість переглядати колекцію. Тоді вони можуть залежати від того, що вони можуть змінити список. Тоді пізніше, якщо ви вирішите перетворити фактичне сховище даних з a List<T>на a Dictionary<T,U>і виставити ключі словника як фактичне значення для властивості (мені це доводилося робити раніше). Тоді споживачі, які очікували, що їх зміни будуть відображені у вашому класі, більше не матимуть такої можливості. Це велика проблема! Якщо ви виставите це List<T>як a, IEnumerable<T>ви можете комфортно передбачити, що ваша колекція не змінюється зовні. Це одна з повноважень виставляти List<T>як будь-який із вищезазначених інтерфейсів.

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

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

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


9

Коротка відповідь:

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

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

Прочитайте трохи про спадщину та поліморфізм .


8

Ось приклад: колись у мене був проект, де наші списки стали дуже великими, і в результаті фрагментація великої купи об’єктів шкодила продуктивності. Ми замінили List на LinkedList. LinkedList не містить масиву, тому раптом ми майже не використовували велику купу об’єктів.

Здебільшого ми використовували списки як IEnumerable<T>і в будь-якому випадку, тому подальших змін не потрібно. (І так, я б рекомендував оголосити посилання як IEnumerable, якщо все, що ви робите, це їх перерахування.) У кількох місцях нам знадобився індексатор списку, тому ми написали неефективну IList<T>обгортку навколо пов'язаних списків. Індексатор списків нам потрібен був рідко, тому неефективність не була проблемою. Якби це було так, ми могли б надати якусь іншу реалізацію IList, можливо, як колекцію досить малих масивів, що було б ефективніше індексувати, уникаючи при цьому великих об’єктів.

Зрештою, можливо, вам доведеться замінити реалізацію з будь-якої причини; продуктивність - це лише одна можливість. Незалежно від причини, використання найменш похідного типу з можливих зменшить необхідність змін у коді при зміні конкретного типу часу виконання ваших об’єктів.


3

Усередині методу слід використовувати varзамість IListабо List. Коли джерело даних зміниться, а не метод, а onlySomeIntsметод, він виживе.

Причиною використання IListзамість Listпараметрів є те, що багато речей реалізують IList(List та [], як два приклади), але реалізує лише одне List. Простіше кодувати інтерфейс.

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


13
Казати, що він повинен використовувати "var", є абсолютно неправильним. Неважливо, що він там використовує - це питання стилю. Це не впливає на підпис методу і встановлюється в камені під час компіляції. Натомість ви повинні допомагати йому подолати свою плутанину щодо оголошення свого місцевого жителя як IList foo = new List - тут явно лежить його плутанина.
x0n

Отже, ви в основному говорите про те, щоб взяти IList просто для того, якщо вони хочуть надіслати масив, який їм не потрібно робити .ToList спочатку? Як щодо повернення? Я не розумію, що ви маєте на увазі під "методом виживе" при використанні vars? Я знаю, що це буде трохи більше роботи, але чи не доведеться вам просто змінити це на новий тип? То, можливо, IList <int> до IList <String>?
chobo2

Правда, сенс "використовувати var" - це, скоріше, пропозиція не турбуватися про це всередині самого методу і більше зосереджуватись на тому, як він виглядає для споживачів.
Bryan Boettcher

Я погоджуюсь з @ x0n: var дуже сильно використовується і не допоможе зробити щось більш зрозумілим.
Той Чак Гай

1

Використання IList замість List значно полегшує написання модульних тестів. Це дозволяє використовувати бібліотеку "Знущання" для передачі та повернення даних.

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

Розглянемо (надуманий) випадок, коли у мене є об’єкт даних, який реалізує IList.

public class MyDataObject : IList<int>
{
    public void Method1()
    {
       ...
    }
    // etc
}

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

У вашому прикладі IEnumerable - кращий вибір, як ви думали.


1

Завжди корисно максимально зменшити залежності між кодом.

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

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

Однак, якщо ви не пишете фреймворк, може бути вигідним передавати параметр з найслабшими можливими типами (тобто базовими класами, інтерфейсами або навіть делегатами) і повертати конкретні типи. Це дає змогу абоненту зробити якомога більше із повернутим об’єктом, навіть якщо він переданий як інтерфейс. Однак це робить метод більш крихким, оскільки будь-яка зміна типу поверненого об'єкта може порушити код виклику. Однак на практиці це, як правило, не є основною проблемою.


1

Ви приймаєте інтерфейс як параметр методу, оскільки це дозволяє абоненту подавати різні конкретні типи як аргументи. Враховуючи ваш приклад методу LogAllChecked, параметр someClasses може бути різного типу, і для людини, що пише метод, усі можуть бути еквівалентними (тобто ви повинні написати точно такий самий код незалежно від типу параметра). Але для людини, яка викликає метод, це може мати величезне значення - якщо у них є масив, і ви просите список, їм слід змінити масив на список або vv при кожному виклику методу, що сумарно витрачає час і від програміста, і від POV продуктивності.

Повернення інтерфейсу чи конкретного типу залежить від того, що ви хочете дозволити вашим абонентам робити з об’єктом, який ви створили - це рішення дизайну API, і немає жорсткого правила. Ви повинні порівнювати їх здатність повною мірою використовувати об’єкт та їх здатність легко використовувати частину функціональних можливостей об’єктів (і, звичайно, ХОЧИТЕ, щоб вони повною мірою використовували об’єкт). Наприклад, якщо ви повертаєте IEnumerable, ви обмежуєте їх ітерацією - вони не можуть додавати або видаляти елементи з вашого об'єкта, вони можуть діяти лише проти об'єктів. Якщо вам потрібно виставити колекцію поза класом, але не хочете дозволити абоненту змінити колекцію, це один із способів зробити це. З іншого боку, якщо ви повертаєте порожню колекцію, яку ви очікуєте / хочете заповнити,


0

Ось моя відповідь у цьому світі .NET 4.5+ .

Використовуйте IList <T> та IReadonlyList <T> ,
              замість List <T> , оскільки ReadonlyList <T> не існує.

IList <T> виглядає так узгоджено з IReadonlyList <T>

  • Використовуйте IEnumerable <T> для мінімальної експозиції (властивість) або вимоги (параметра), якщо foreach - це єдиний спосіб його використання.
  • Використовуйте IReadonlyList <T>, якщо вам також потрібно виставити / використовувати Count та [] індексатор.
  • Використовуйте IList <T>, якщо ви також дозволяєте абонентам додавати / оновлювати / видаляти елементи

оскільки List <T> реалізує IReadonlyList <T> , він не потребує явного кастингу.

Приклад класу:

// manipulate the list within the class
private List<int> _numbers;

// callers can add/update/remove elements, but cannot reassign a new list to this property
public IList<int> Numbers { get { return _numbers; } }

// callers can use: .Count and .ReadonlyNumbers[idx], but cannot add/update/remove elements
public IReadOnlyList<int> ReadonlyNumbers { get { return _numbers; } }
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.