Подтвердить что ты не робот

Рекомендации .NET по дизайну предлагают вернуть List <T> поверх IEnumerable <T>?

Контекст: я отправил электронное письмо своим коллегам, в котором рассказывал им о Enumerable.Empty<T>() как о возврате пустых коллекций, не делая что-то вроде return new List<T>();. Я получил ответ, говорящий, что недостатком является то, что он не выдает определенный тип

Это представляет собой крошечную проблему. Всегда полезно быть как можно более конкретным относительно вашего типа возврата * (поэтому, если вы возвращаете List < > , введите этот тип возврата); потому что такие вещи, как "Списки" и "Массивы", имеют дополнительные методы, которые полезны. Кроме того, это может быть полезно при определении производительности при использовании коллекции из вызывающего метода.

Трюк ниже, к сожалению, заставляет вас возвращать IEnumerable, который примерно как нельзя более конкретный, не так ли?: (

* Это на самом деле из .NET Design Guidelines. Полагаемые причины в руководящих принципах те же, что и я упоминаю здесь, я считаю.

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

DO возвращает подкласс Collection<T> или ReadOnlyConnection<T> из очень часто используемых методов и свойств.

С последующим фрагментом кода, но не более оправданием вообще.


Итак, чтобы быть сказанным, является ли это реальным и принятым руководством (как описано в первой цитате блока)? Или это было неверно истолковано? Все другие ответы на вопросы, которые я мог найти, отвечают за выбор типа IEnumerable<T>. Возможно, исходные рекомендации .NET устарели?

Или, может быть, это не так понятно? Есть ли какие-то компромиссы? Когда было бы хорошей идеей вернуть более конкретный тип? Всегда рекомендуется возвращать конкретный общий тип или только для возврата более конкретного интерфейса, такого как IList<T> и ReadOnlyCollection<T>?

4b9b3361

Ответ 1

Есть причины для обоих стилей:

  • Будь конкретным. Вы предоставляете гарантию абонентам, что вы всегда будете возвращать этот тип. Даже когда ваша реализация меняется. Можете ли вы сохранить обещание? Если да, будьте конкретны, чтобы позволить абонентам пользоваться преимуществами. Более производные типы имеют больше возможностей.
  • Будьте общим. Если вы, вероятно, измените базовую реализацию рассматриваемого метода или свойства, вы не можете обещать List<T>. Возможно, вы можете пообещать IList<T> или пользовательский класс коллекции (который вы можете изменить позже).

.NET framework BCL идет либо с массивами, либо с обычными наборами коллекций. Им необходимо предоставить API с 100% стабильностью, поэтому им нужно быть осторожным.

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

Существует только одна вещь, которая всегда ошибочна: говорить, что всегда следует делать (1) или (2). Всегда всегда неправильно.

Вот пример для случая, когда определенность является правильным выбором:

    public static T[] Slice<T>(this T[] list, int start, int count)
    {
        var result = new T[count];
        Array.Copy(list, start, result, 0, count);
        return result;
    }

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

С другой стороны, метод возврата всех пользователей из некоторого постоянного хранилища может сильно измениться внутри. Данные могут быть даже больше, чем доступная память, которая требует потоковой передачи. Вероятно, мы должны выбрать IEnumerable<User>.

Ответ 2

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

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

Другое, но, на мой взгляд, равноправная альтернативная философия будет заключаться в том, что возврат более общих типов просто делает вашу библиотеку менее полезной, и поэтому вы должны возвращать более конкретные типы. Например, агрессивно возвращая IEnumerable<T> все время приведет к сценарию, в котором конечные пользователи вашей библиотеки постоянно звонят ToArray(), ToList() и т.д. Я считаю, что это основной момент, который ваш коллега делал, когда писал, что "такие вещи, как списки и массивы, имеют дополнительные методы, которые полезны".

Я иногда слышал, как последний подход пропагандировался словами: "Будьте конкретны в том, что вы предлагаете, но общий в том, что вы принимаете". Другими словами, возьмите общие параметры, но предложите конкретные возвращаемые значения. Согласны ли вы с этим принципом или нет, легко по крайней мере увидеть причину этого.

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

С другой стороны, у вас может быть какой-то метод, который всегда возвращает T[], который вы выберете, только как IEnumerable<T>, хотя вероятность того, что эта реализация изменится, очень низкая. В этом случае выбор более типичного типа может быть приятным для вас эстетически, но вряд ли он действительно приносит пользу кому-либо и фактически лишает результат очень полезной функциональности: произвольный доступ.

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

Ответ 3

Возвращаемый список вместо IEnumerable (или, по крайней мере, IList) по существу нарушает Принцип либерализации Лискова

Возврат "Список" блокирует вас, чтобы вернуть этот тип, и не позволяет вам реорганизовать внутренности метода на потенциально более эффективную реализацию (вы обещали вернуть List, но теперь вы хотите использовать "yield" - oops, не может сделать это без внешнего кода).

Во многих случаях (чаще?) преобладающим требованием такого рода методов является повторение результатов и выполнение чего-то... IEnumerable в точности подходит для этих целей.

Если пользователь должен иметь изменяемый набор, это выполняется тривиально с помощью "ToList()" или других расширений на IEnumerable на другом конце.

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

Ответ 4

IMO важно помнить, что List<T> и IEnumerable<T> имеют разную семантику.

Возврат IEnumerable<T> просто дает вызывающей стороне последовательность того, что можно перечислить (и, возможно, превратить в коллекцию). Однако нет гарантии, что последовательность была реализована. Поэтому возврат IEnumerable<T> поддерживает отложенное выполнение. Кроме того, он неявно "читается только".

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