Pull to refresh

Принятого не воротай: Enumerable vs List

Reading time4 min
Views20K

Когда-то я работал в команде, где слегка недолюбливали LINQ, за то, что такой код якобы сложно отлаживать. У нас была договоренность: после каждой цепочки LINQ, разработчик создает локальную переменную, в которую записывает результат ToArray(). Независимо от того, потребуется ли массив далее по методу, или он работает только с IEnumerable. Перед return, результат также приводился к массиву, кажется, во всей кодовой базе не было методов, возвращающих или принимающих коллекцию, отличную от массива.

Бородатое легаси! - подумаете вы и будете правы. Однако, несмотря то, что прошло много лет, с тех пор, как LINQ стал использоваться повсеместно, а IDE позволяют смотреть данные в отладке, некоторые разработчики все еще плохо представляют себе критерии выбора принимаемого и возвращаемого типа, если речь заходит о коллекциях.

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

Предпочитайте абстракции

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

Lazy loading

Вопрос - а прогрузились ли данные(например в IEnumerable) или нет, сам по себе не касается типа. Метод может возвращать как свою реализацию IList, так и стандартную, при этом с отложенной загрузкой данных. Информировать пользователя о том, используется ли lazy loading, посредством возвращаемого типа — плохая идея. Вы обременяете тип несвойственными ему обязанностями. Комментарий, либо специфичный постфикс 'Lazy' в названии метода, будут более удачным решением, если это не ясно из контекста.

IReadOnlyCollection

Несмотря на то, что по тем или иным причинам, у нас нет IArray, у нас есть IReadOnlyCollection, добавляющий к перечислению свойство-размер. 

namespace System.Collections.Generic 
{ 
  public interface IReadOnlyCollection : IEnumerable, IEnumerable 
  { 
    int Count { get; } 
  }
}

Соответствующий класс-враппер был добавлен в версии фреймворка 4.5, для удобного создания read-only коллекций. К нему легко можно преобразовать как Array, так и List, поскольку оба они реализуют IList.

С IEnumerable дела обстоят хуже… Здесь, чтобы получить IReadOnlyCollection, вам так или иначе придётся сначала получить List. Таким образом, де-факто стандартом здесь будет являться List.

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

Возвращайте пустые коллекции вместо null

Кажется, уже все про это знают, тем не менее, я то и дело, иногда, то тут, то там, получаю null. Это побуждает меня и коллег добавлять проверки на null, что сводит к минимуму комфортную работу с пустыми коллекциями. Задумайтесь - null это не 0 элементов. Возвращая 1 раз null, вы навсегда обрекаете пользователя штамповать проверки на null, там, где они избыточны. Например:

 if(myEnumerable != null) 
 { 
   foreach(var item in myEnumerable) 
   { 
   } 
 }  

Хуже, но более лаконично:

foreach(var item in myEnumerable ?? Enumerable.Empty<T>()) 
{
}

IEnumerable/ICollection/IList

Для начала, вкратце, что есть что:

IEnumerable

собственно, неизменяемая коллекция-перечисление, вам доступен только перечислитель

IReadOnlyCollection : IEnumerable

неизменяемая коллекция-перечисление, вам доступен перечислитель и размер

ICollection : IEnumerable

коллекция с возможностью добавлять и удалять элементы, также доступен размер и признак изменяемости(IsReadOnly)

IReadOnlyList : IReadOnlyCollection

неизменяемая коллекция с порядком следования элементов, вам доступен индексатор

IList : ICollection

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

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

Возвращайте максимально конкретный тип, нет смысла прятать за обобщенный интерфейс те данные, которыми вы уже располагаете, в результате работы метода. Если вы имеете массив, то вы ничего не теряете, возвращая IReadOnlyCollection. Возвращая IEnumerable вы скрываете знание о размере и в случае, если оно понадобится пользователю, прийдется изменять сигнатуру метода, а если это невозможно — создавать дублирующий метод-перегрузку. Если результатом работы вашего метода является коллекция фиксированного размера и вы хотите избежать lazy loading, имеет смысл вернуть IList или ICollection, если вам важно указать пользователю на неизменяемость — их read-only аналоги.

Web API и HTTP

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

На самом деле, по HTTP ваша коллекция уедет как JSON - сериализованный массив, вся целиком. И приедет, если мы говорим о популярных десериализаторах(Newtonsoft.Json, System.Text.Json), не иначе как List. В данном случае нет никакого смысла отдавать\принимать что-то другое. Указывая IEnumerable в response контроллера вы только усложняете понимание кода.


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

Буду рад поправкам и дополнениям, рекомендую ознакомиться с Framework Design Guidelines for Collections.

Tags:
Hubs:
Total votes 9: ↑7 and ↓2+5
Comments25

Articles