Возвращает IList <T> хуже, чем возврат T [] или List <T>?

Ответы на такие вопросы: List <T> или IList <T> всегда, похоже, согласны с тем, что возврат интерфейса лучше, чем возврат конкретной реализации коллекции. Но я борюсь с этим. Создание экземпляра интерфейса невозможно, поэтому, если ваш метод возвращает интерфейс, он фактически все еще возвращает определенную реализацию. Я немного экспериментировал с этим, написав два небольших метода:

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 в List, чтобы иметь возможность добавлять значения без риска ошибок. Конечно, они могут сделать typecheck, чтобы узнать, имеют ли они дело с List или Array, но если они этого не делают, и они хотят добавить элементы в коллекцию , у них нет другого выбор для копирования IList в List, даже если он уже является List. Если массив никогда не отображается как IList?

Другая моя забота основана на принятом ответе связанного вопроса (акцент мой):

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

Если вы просто используете его внутренне, вам может быть не так много, и использование List может быть в порядке.

Представьте, что кто-то действительно использовал мой IList<T>, который они получили из моего метода ExposeListIlist(), как раз для добавления/удаления значений. Все работает нормально. Но теперь, как и в ответе, потому что возврат интерфейса более гибкий, я возвращаю массив вместо списка (без проблем на моей стороне!), Тогда они готовы к лечению...

TL;DR:

1) Нарушение интерфейса вызывает ненужные приведения? Не имеет значения?

2) Иногда, если пользователи библиотеки не используют бросок, их код может сломаться при изменении вашего метода, хотя метод остается в полном порядке.

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

Ответ 1

Возможно, это напрямую не отвечает на ваш вопрос, но в .NET 4.5+ я предпочитаю следовать этим правилам при разработке общедоступных или защищенных API:

  • do return IEnumerable<T>, если доступно только перечисление;
  • do return IReadOnlyCollection<T>, если доступны как перечисление, так и количество элементов;
  • do return IReadOnlyList<T>, если перечисление, количество элементов и индексный доступ доступны;
  • do return ICollection<T>, если доступно перечисление, количество элементов и модификация;
  • do return IList<T>, если перечислены, количество элементов, индексированный доступ и модификация доступны.

Последние два варианта предполагают, что этот метод не должен возвращать массив как реализацию IList<T>.

Ответ 2

Нет, потому что потребитель должен знать, что именно IList:

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

Вы можете проверить IList.IsFixedSize и IList.IsReadOnly и сделать то, что вы хотите с ним.

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

Подробнее, если вы хотите принять решение о возвращении интерфейса


UPDATE

Копаем больше, и я обнаружил, что IList<T> не реализует IList, а IsReadOnly доступен через базовый интерфейс ICollection<T> но для IList<T> нет IsFixedSize. Узнайте больше о почему generic IList < > не наследует non-generic IList?

Ответ 3

Как и во всех вопросах "интерфейс против реализации", вам нужно понять, что означает публичный член: он определяет публичный 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> вместо этого.

Ответ 4

Будьте осторожны с кавычками, которые выведены из контекста.

Возвращение интерфейса лучше, чем возврат конкретной реализации

Эта цитата имеет смысл только в том случае, если она используется в контексте принципов SOLID. Существует 5 принципов, но для целей этого обсуждения мы просто поговорим о последних 3.

Принцип инверсии зависимостей

нужно "Зависить от абстракций. Не зависеть от конкреций".

По-моему, этот принцип наиболее трудно понять. Но если вы внимательно посмотрите на цитату, это очень похоже на вашу оригинальную цитату.

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

Это все еще немного запутанно, но если мы начнем применять другие принципы вместе, это начинает приобретать больше смысла.

Принцип подстановки Лискова

"в программе должны быть заменены экземплярами их подтипов без изменения правильности этой программы."

Как вы указали, возвращение Array явно отличается поведением к возврату List<T>, хотя оба они реализуют IList<T>. Это, безусловно, нарушение LSP.

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

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

"многие клиентские интерфейсы лучше, чем один универсальный интерфейс".

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

К сожалению, интерфейсы в платформе .NET(особенно ранние версии) не всегда являются идеальными клиентскими интерфейсами. Хотя, как отметил @Dennis в своем ответе, в .NET 4.5 + есть намного больше вариантов.

Ответ 5

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

Допустимыми причинами использования интерфейса могут быть:

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

  • Вы хотите предоставить некоторые общие функции, независимые от вашей иерархии классов, безопасным способом. Объекты разных базовых типов, которые должны предлагать одни и те же методы, будут реализовывать ваш интерфейс.

Можно утверждать, что 1 и 2 в основном по той же причине. Это два разных сценария, которые в конечном итоге приводят к одной и той же потребности.

"Это контракт". Если контракт с вами и ваше приложение закрыто как по функциональности, так и по времени, часто нет смысла использовать интерфейс.