Почему IEnumerable <T>.ToList <T>() возвращает List <T> вместо IList <T>?

Метод расширения ToList() возвращает a List<TSource>. Следуя той же схеме, ToDictionary() возвращает a Dictionary<TKey, TSource>.

Мне любопытно, почему эти методы не вводят возвращаемые значения как IList<TSource> и IDictionary<TKey, TSource> соответственно. Это кажется еще более странным, потому что ToLookup<TSource, TKey> выводит возвращаемое значение как интерфейс вместо фактической реализации.

Рассматривая источник этих методов расширения с помощью dotPeek или другого декомпилятора, мы видим следующую реализацию (показывая ToList(), потому что она короче):

public static List<TSource> ToList<TSource>(this IEnumerable<TSource> source) { 
   if (source == null) throw Error.ArgumentNull("source");
   return new List<TSource>(source); 
}

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

Мне любопытно, потому что расширения IEnumerable<> очень согласованы в своих подписях, за исключением этих двух случаев. Я всегда думал, что это немного странно.

Кроме того, чтобы сделать вещи еще более запутанными, документация для ToLookup() гласит:

Создает поиск из IEnumerable в соответствии с указанная функция выбора ключа.

но возвращаемый тип ILookup<TKey, TElement>.

В Edulinq Джон Скит упоминает, что тип возврата List<T> вместо IList<T>, но не касается темы далее. Обширный поиск не дал ответа, поэтому я прошу вас:

Есть ли какое-либо конструкторское решение, не набирающее возвращаемые значения в качестве интерфейсов, или это просто случайность?

Ответ 1

Возврат List<T> имеет то преимущество, что те методы List<T>, которые не являются частью IList<T>, легко используются. Есть много вещей, которые вы можете сделать с List<T>, который вы не можете сделать с помощью IList<T>.

Напротив, Lookup<TKey, TElement> имеет только один доступный метод, который ILookup<TKey, TElement> не имеет (ApplyResultSelector), и вы, вероятно, все равно не сможете его использовать.

Ответ 2

Такие решения могут быть произвольными, но я полагаю, что ToList() возвращает List<T>, а не интерфейс, потому что List<T> реализует IList<T>, но он добавляет другие члены, не присутствующие в регулярном IList<T> -типированном объекте.

Например, AddRange().

Посмотрите, что должен реализовать IList<T> (http://msdn.microsoft.com/en-us/library/5y536ey6.aspx):

public interface IList<T> : ICollection<T>, 
    IEnumerable<T>, IEnumerable

И List<T> (http://msdn.microsoft.com/en-us/library/6sh2ey19.aspx):

public class List<T> : IList<T>, ICollection<T>, 
    IList, ICollection, IReadOnlyList<T>, IReadOnlyCollection<T>, IEnumerable<T>, 
    IEnumerable

Возможно, ваш собственный код не требует IReadOnlyList<T>, IReadOnlyCollection<T> или ICollection, но другие компоненты на платформе .NET Framework и других продуктах могут полагаться на более специализированный объект списка и поэтому команда разработчиков .NET решила не возвращайте интерфейс.

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

Ответ 3

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

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

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

Затем мы спрашиваем себя, какие преимущества есть для возвращения IList. Ну, мы могли бы вернуть некоторую другую реализацию списка, но, как упоминалось ранее, это может иметь очень пагубные последствия, почти наверняка намного больше, чем можно было бы извлечь из использования любого другого типа. Это может дать вам теплые пушистики использовать интерфейс вместо реализации, но даже это то, что я не чувствую, является хорошим менталитетом (вообще или) в этом контексте. Как правило, возврат интерфейса обычно не предпочтительнее возврата конкретной реализации. "Будьте либеральны в том, что вы принимаете и конкретны в том, что вы предоставляете". Параметры для ваших методов должны, по возможности, быть интерфейсами, определяющими наименьшее количество функциональности, которое вам нужно, чтобы ваш вызывающий может передать любую реализацию, которая делает то, что вам нужно, и вы должны указать как конкретную реализацию, так как вызывающий "разрешено" видеть, чтобы они могли делать столько же с результатом, каким способен этот объект. Передача интерфейса ограничивает это значение, которое иногда является чем-то, что вы хотите сделать.

Итак, теперь мы переходим на: "Зачем возвращать ILookup, а не Lookup?" Ну, сначала Lookup не является публичным классом. В System.Collections.* нет Lookup. Класс Lookup, открытый через LINQ, не предоставляет публичным конструкторам. Вы не можете использовать класс, кроме ToLookup. Он также не предоставляет никаких функций, которые еще не открываются через ILookup. В этом конкретном случае они специально разработали интерфейс именно для этого точного метода (ToLookup), а класс Lookup - это класс, специально предназначенный для реализации этого интерфейса. Из-за всего этого практически все обсуждаемые вопросы о List просто не применимы здесь. Было бы проблемой вернуть Lookup вместо этого, нет, на самом деле. В этом случае это действительно не имеет большого значения в любом случае.

Ответ 4

По-моему, возвращение List<T> оправдано тем фактом, что имя метода говорит ToList. В противном случае его нужно было бы назвать ToIList. Целью этого метода является преобразование неспецифического IEnumerable<T> в конкретный тип List<T>.

Если у вас был метод с неспецифическим именем типа GetResults, тогда мне бы понравился такой тип возврата, как IList<T> или IEnumerable<T>.


Если вы посмотрите на реализацию класса Lookup<TKey, TElement> с отражателем, вы увидите много членов internal, которые доступны только для LINQ. Нет открытого конструктора, и объекты Lookup неизменяемы. Поэтому не было бы никакого преимущества при непосредственном экспонировании Lookup.

Lookup<TKey, TElement> класс кажется своего рода LINQ-внутренним и не предназначен для общественного использования.

Ответ 5

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

Ответ 6

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

Ответ 7

Потому что List<T> фактически реализует ряд интерфейсов, а не только IList:

public class List<T> : IList<T>, ICollection<T>, IList, ICollection, IReadOnlyList<T>, IReadOnlyCollection<T>, IEnumerable<T>, IEnumerable{

}

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

Если вы хотите вернуть IList, ничто не мешает вам иметь вашу собственную простую оболочку:

public static IList<TSource> ToIList<TSource>(this IEnumerable<TSource> source)
{
    if (source == null) throw new ArgumentNullException(source);
    return source.ToList();
}

Ответ 8

Короткий ответ заключается в том, что в целом возврат наиболее специфического типа, доступного, рекомендуется авторитетным Руководством по разработке каркаса. (извините, у меня нет цитаты под рукой, но я помню это четко, так как она торчала в отличие от рекомендаций сообщества Java, которые предпочитают обратное).

Это имеет смысл для меня. Вы всегда можете делать это, например. IList<int> list = x.ToList(), только автор библиотеки должен быть заинтересован в возможности поддержки конкретного типа возврата.

ToLookup<T> является уникальным в толпе. Но совершенно в рамках рекомендаций: это самый специфический тип, который авторы библиотеки готовы поддержать (как указывали другие, конкретный тип Lookup<T> представляется скорее внутренним типом, не предназначенным для общественного использования).

Ответ 9

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

Возвращая конкретный List<T>, который реализует IList<T>, только дает потребителю метода больше информации. Вот что реализует объект List (через MSDN):

[SerializableAttribute]
public class List<T> : IList<T>, ICollection<T>, IList, ICollection, 
    IReadOnlyList<T>, IReadOnlyCollection<T>, IEnumerable<T>, IEnumerable

Возврат в качестве List<T> дает нам возможность вызвать участников на всех этих интерфейсах в дополнение к List<T>. Например, мы могли бы использовать List.BinarySearch(T) для List<T>, как это существует в List<T>, но не в IList<T>.

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

Ответ 10

Если функция возвращает вновь построенный неизменяемый объект, вызывающий объект обычно не заботится о возврате точного типа, если он способен хранить фактические данные, которые он содержит. Например, функция, которая должна возвращать IImmutableMatrix, как правило, может возвращать ImmutableArrayMatrix, поддерживаемый частным массивом, но если все ячейки содержат нули, она может вместо этого возвращать ZeroMatrix, поддерживаемый только Width и Height (с геттером, который просто возвращает ноль все время). Вызывающему было бы все равно, была ли ему дана матрица ImmutableArrayMatrix или ZeroMatrix; оба типа позволяли бы читать все их ячейки и гарантировать, что их ценности никогда не изменятся, и то, о чем бы заботился вызывающий.

С другой стороны, функции, возвращающие вновь построенные объекты, которые допускают открытую мутацию, обычно должны возвращать точный тип, который будет ожидать вызывающий. Если не будет средств, с помощью которых вызывающий может запрашивать разные типы возврата (например, вызывая ToyotaFactory.Build("Corolla") versus ToyotaFactory.Build("Prius")), нет причин для объявленного типа возврата быть чем-то другим. В то время как фабрики, которые возвращают неизменяемые объекты хранения данных, могут выбирать тип, основанный на данных, которые должны содержаться, фабрики, которые возвращают свободно изменяемые типы, не смогут понять, какие данные могут быть помещены в них. Если разные абоненты будут иметь разные потребности (например, вернувшись к существующему примеру, некоторые потребности вызывающих абонентов будут удовлетворены с помощью массива, а другие - не будут) им должен быть предоставлен выбор методов factory.

Кстати, что-то вроде IEnumerator<T>.GetEnumerator() - это немного особый случай. Возвращаемый объект будет почти всегда изменяться, но только очень сильно ограниченным образом; действительно, ожидается, что возвращаемый объект, независимо от его типа, будет иметь ровно одну часть изменяемого состояния: его положение в последовательности перечисления. Хотя ожидается, что IEnumerator<T> будет изменчивым, части его состояния, которые будут меняться в реализациях производного класса, не являются.