Зачем использовать 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, поскольку это то, что кажется стандартом для использования с nhibernate. В противном случае я мог бы вернуть 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);
                    }
                }
            }

у меня возникнут проблемы с этим? Поскольку они не могли передать массив (имеющий фиксированный размер)? Может быть, лучше для конкретного списка?

chobo2
источник

Ответы:

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 03
3
@phoog: Учитывая, откуда пришел Эрик, неудивительно, что он более осторожен в отношении внесения изменений. Но это определенно верная точка зрения.
Очень трудный. Вроде есть люди с обеих сторон (верните голый мин или нет). Я больше понимаю, почему вы делаете это для параметров, которые помогают избежать ненужного преобразования. Я все еще не уверен, что делать с возвращаемым типом.
chobo2 03
@ 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>как an, 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 03
Итак, в основном вы говорите, что нужно использовать IList просто потому, что если они хотят отправить массив, который им не нужно делать сначала .ToList? Как насчет его возврата? Я не понимаю, что вы имеете в виду под «методом выживет» при использовании варов? Я знаю, что это будет немного больше работы, но разве вам просто не нужно будет изменить это на новый тип? Так, может быть, IList <int> на IList <String>?
chobo2
Правда, смысл «использования var» был скорее предложением не беспокоиться об этом внутри самого метода и больше сосредоточиться на том, как он выглядит для потребителей.
Bryan Boettcher
Я согласен с @ x0n: var используется слишком часто и ничего не проясняет.
That Chuck Guy
1

Использование IList вместо List значительно упрощает написание модульных тестов. Это позволяет использовать библиотеку «Mocking» для передачи и возврата данных.

Другая общая причина использования интерфейсов - предоставить пользователю минимальный объем знаний, необходимых для работы с объектом.

Рассмотрим (надуманный) случай, когда у меня есть объект данных, реализующий IList.

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

Ваши функции, указанные выше, заботятся только о возможности перебора списка. В идеале им не нужно знать, кто реализует этот список или как они его применяют.

В вашем примере IEnumerable - лучший выбор, как вы думали.

Нафанаил
источник
1

Всегда полезно максимально уменьшить зависимости между вашим кодом.

Имея это в виду, имеет смысл передавать типы с наименьшим возможным количеством внешних зависимостей и возвращать то же самое. Однако это может отличаться в зависимости от видимости ваших методов и их сигнатур.

Если ваши методы являются частью интерфейса, методы должны быть определены с использованием типов, доступных для этого интерфейса. Конкретные типы, вероятно, будут недоступны для интерфейсов, поэтому им придется возвращать неконкретные типы. Вы бы захотели сделать это, например, если бы создавали фреймворк.

Однако, если вы не пишете фреймворк, может быть полезно передать параметр с самыми слабыми из возможных типов (то есть базовыми классами, интерфейсами или даже делегатами) и вернуть конкретные типы. Это дает вызывающему объекту возможность делать с возвращенным объектом как можно больше, даже если он приведен как интерфейс. Однако это делает метод более хрупким, так как любое изменение типа возвращаемого объекта может нарушить вызывающий код. Однако на практике это обычно не является большой проблемой.

Ричард
источник
1

Вы принимаете интерфейс в качестве параметра для метода, потому что это позволяет вызывающему объекту передавать различные конкретные типы в качестве аргументов. Учитывая ваш пример метода LogAllChecked, параметр someClasses может быть разных типов, и для человека, пишущего метод, все могут быть эквивалентными (то есть вы должны написать один и тот же код независимо от типа параметра). Но для человека, вызывающего метод, это может иметь огромное значение - если у них есть массив, и вы запрашиваете список, они должны изменить массив на список или vv при каждом вызове метода, полная трата время и от программиста, и от перформанса POV.

Возвращаете ли вы интерфейс или конкретный тип, зависит от того, что вы хотите разрешить своим вызывающим лицам делать с созданным вами объектом - это дизайнерское решение API, и здесь нет жесткого правила. Вы должны взвесить их способность в полной мере использовать объект с их способностью легко использовать часть функциональных возможностей объекта (и, конечно же, ХОТИТЕ ли вы, чтобы они использовали объект в полной мере). Например, если вы возвращаете IEnumerable, вы ограничиваете их итерацией - они не могут добавлять или удалять элементы из вашего объекта, они могут действовать только против объектов. Если вам нужно предоставить коллекцию вне класса, но вы не хотите позволять вызывающей стороне изменять коллекцию, это один из способов сделать это. С другой стороны, если вы возвращаете пустую коллекцию, которую ожидаете / хотите, чтобы они заполнялись,

Jmoreno
источник
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; } }
детали
источник