Как стать автором
Обновить

Комментарии 25

Корректорское замечание.
На протяжении статьи, в том числе в подзаголовках, несколько раз упоминается IRealonly. Если это не то же самое, что IReadonly, поясните, пожалуйста, что это. Если это опечатка и имелось в виду IReadonly, исправьте, пожалуйста.

Благодарю!
IReadOnlyList: IReadOnlyCollection — неизменяемая коллекция с порядком следования элементов, вам доступен индексатор
Строго говоря любой IEnumerable определяет порядок следования, тут просто есть доступ по индексу элемента. Причём никто не мешает сделать реализацию, где индексы будут расставлены случайно (зачем — отдельный вопрос)

Насчет производительности. Имею опыт, делал профайлинг одного старого проекта (на Яве правда) обьемом около 550т строк. Методы возвращали массивы, которые создавались из List перед return. Вот на всей кодовой базе, в реальных условиях, с синтетической нагрузкой приближенной к боевой, разница в скорости между вариантом "до рефакторинга" и "после", на 8й Яве, укладывалась с статистическую погрешность. Размеры коллекций были от 100 до 100т элементов.

А память вы не измеряли?
Вообще хорошая идея — взять какой-нибудь небольшой проект на .Net, повставлять приведений к массиву и качественно измерить потребление ресурсов.
Для коллекции ссылочных типов перегон IEnumerable через ToList() означает копирование ссылок из итератора в лист, вряд ли вы в реальном проекте заметите какие-то существенные изменения в использовании памяти, другое дело — типы значимые. Но это предположение никак не отменяет факт, что перечисления нужно использовать правильно :)
Измерял, никакого существенного изменения за время теста, а это около 6..7 часов на 1 тест-кейс (требуемый уровень RPS например), не было.
Общее впечатление после недели опыта в разных режимах: с погрешностью в +- 2..3% абсолютно одинаковое поведение было. Тот проект начинался еще на jdk1.3/1.4, вот там я думаю разница уже запросто могла бы быть.
ну, List и массив хранят данные. там может быть очень много оптимизаций при преобразовании одного в другое.
IEnumerable не обязательно хранит данные, в случае LINQ там целые цепочки их получения, которые могут упираться в коллекцию в оперативки, в генеративную функцию, в базу данных… Во всех этих случаях могут быть самые разные преобразования в тот момент, когда ты непосредственно генерируешь коллекцию. Цепочка может сложиться в оптимизированный запрос к БД, в таких случаях преждевременное получение коллекции может породить большее количество данных выгруженных из БД в оперативку.
В .net List является динамическим массивом и внутри у него неонка массив.
Судя по коду, который даёт декомпилятор для mscorlib 4.0 .net 4.6.2 преобразование к массиву (List\<T\>.ToArray()) создаёт новый массив, размером с текущую коллекцию и через Array.Copy копирует в него элементы текущего.
Array.Copy — довольно быстрая штука, отдельная аллокация памяти, в принципе, тоже.
Но делать так 10000+ в цикле точно не стоит, накладные расходы станут ооочень заметны.

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

p.s. как в хабровском редакторе эскейпить угловые скобки?
Что касается рекомендаций к ознакомлению, то можно ознакомится со всей книгой Цвалины Framework Design Guidelines: Conventions, Idioms, and Patterns for Reusable .NET Libraries. Второе издание есть на русском, но в этом году вышло третье издание с асинхронностью и прочими плюшками. Крайне интересно читать, тем более, что прямо в книге разворачиваются холивары по некоторым вопросам.

Вот я вызываю метод
var people = GetPeople(): IEnumerable;
Дальше мне надо пройтись по people несколько раз. Скажем, банально:
return new {
All: people,
Tall: GetTallPeople(people),
Count: people.Count(),
}

Вот как мне быть — сразу ее в ToArray() бахнуть, или рассчитывать на то, что там никаких тяжелых ленивых вычислений не делается, и она у меня точно два раза не будет считаться?


Т.к. если кидаться IEnumerable между методами постоянно, такое будет то и дело, я считаю что IEnumerable должен жить внутри методов, ни возвращать его, ни принимать его — не надо. За исключением случаев, когда ленивость — подразумевается и понятна.


Я вообще везде, где не нужна ленивость, кидаюсь массивами. Массивы удобнее чем всякие IReadonlyCollection — хотя бы тем что всем понятнее, и букв надо меньше писать.

Если цель писать меньше букв, то тогда вам в какой-нибудь нетипизированный язык)

Гайд фреймвока рекомендует принимать IEnumerable, даже если вам нужен Count(). Здесь подразумевается, что вызывающий скорее всего передаст объект со свойсвом размера и реализация Count() не будет итерировать.

Если вы в методе только перечисляете, при этом уверены что колекция уже подгружена, то можно и не делать ToArray().

А как я могу быть уверенным, видя что мне вернули IEnumerable, что коллекция уже подгружена, или что мне не вернули return someCollection.Select(x => ComputeAnExpensiveHashFunction(x))?

Если даже CLR так не делает — например все Extension методы из System.Collection.LINQ делают противоположное?

И если мне возвращает этот IEnumerable какой-нибудь джунами написанный метод, и они может сейчас массив возвращают, а потом передумают и начнут в БД ходить?

Это же не хаскель, где ленивые списки реализованы так, что если они один раз посчитались кем-то, то второй раз считаться не будут. C# предоставляет такой выбор:
1. IEnumerable — который может запустить ракету в космос сходить в API какой — если по ней проитерироваться, и второй раз сходить туда же — если второй раз проитерироваться.
2. массивы и производные — гарантированно посчитанные и лежащие в памяти

Я — за второе везде, потому что:
— мы таки взяли C#, потому что хотим нанимать много средних разрабов. Иначе мы бы взяли другой язык. Если взял C#, то бери и принцип: «чем тупее — тем ловчее».
— возвращая массив — мы убираем лишний повод что-то сделать не так, не приобретая никаких минусов кроме «илиты на хабрах считают иначе»
— нормального варианта типа «коллекция, которая если один раз проитерировалась — гарантированно не будет это делать второй раз» — нам разрабы .NET не предоставили
Никаких проблем. Если метод возвращает IEnumerable — значит, нет никакой уверенности, что коллекция уже прогружена или не будет изменено поведение потом. Так что однозначно ToArray.
У массивов есть куча важных и неприятных недостатков, из-за которых они не всегда подходят для возвращаемого типа из метода и точно не стоит делать ToArray() на каждом шагу.

1. Массив это объект и подвержен сборке мусора. И если мы возвращаем его из функции, то он сразу попадет в GC Gen 1. А в плохом раскладе и в Gen 2. Массовый ToArray() по всему коду может удвоить нагрузку на GC, что не критично для небольшого проекта, но может быть важно в бекэнде. Чтобы бороться с этим в .Net Core сделан ArrayPool<>, но с ним есть нюанс №2.
2. Получив из ArrayPool<> массив нельзя быть уверенным, что его длина не больше, чем надо и что вы заполните все его элементы. В итоге некоторые программисты начинают возвращать из каждого метода ArraySegment, собственную обертку, отдельно значение count. Либо таки возвращают массив и каждый элемент при переборе сравнивают с null или default. Отдельно остается вопрос, что массивы надо бы и возвращать в пул, а за этим автор метода уже проследить не может.
3. Массив структур физически хранит в себе эти структуры и его создание через ToArray() или CopyTo приведет к массовому копированию. Ну а сам массив потенциально может попасть в LOH, для этого достаточно иметь всего 85000 байт размера. Обработка LOH объектов обычно блокирующая, т.е. останавливает ваше приложение. Есть исключения, но речь не о них.
4. В отличие от IReadOnlyList<>, в массиве можно заменить элемент с каким-то индексом и изначальный метод не узнает об этом.

Моя рекомендация: возвращайте из методов IEnumerable, IReadOnlyCollection, IReadOnlyList (если нужен доступ по индексу). Не заморачивайтесь с массивами, если это не требуется явно. И точно не делайте ToArray по поводу и без повода, от этого код не станет работать быстрее.
Информировать пользователя о том, используется ли lazy loading, посредством возвращаемого типа — плохая идея. Вы обременяете тип несвойственными ему обязанностями.

А вот не факт что они ему несвойственны, использование типов для изоляции поведения — это очень популярный паттерн в ФП, да и в DDD занимаются чем-то похожим.


Другое дело, что IEnumerable<> для этих целей не подходит.


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

Только помните, что подчеркивание неизменяемости является очень даже полезной привычкой.

Только помните, что подчеркивание неизменяемости является очень даже полезной привычкой
Интерфейс IReadOnlyCollection не гарантирует неизменяемости [исходной] коллекции, которую он представляет. Об этом надо помнить, если выполняете несколько раз перечисление коллекции или получаете свойство Count.
На мой взгляд, применение интерфейса оправдано если имеется только один экземпляр класса (объект), владеющий исходной коллекцией, и ему необходимо по запросу предоставить ссылку на элементы коллекции, но не предоставлять методы изменения коллекции. Т.е., реализовать паттерн «Один хозяин — много клиентов».

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


Но интерфейс, который нельзя использовать — никому не нужен, а задача программиста — писать рабочую программу, а не ломать её. Поэтому должно быть соглашение о том, что можно, а чего нельзя делать с передаваемыми объектами. И лично я вижу его следующим образом:


  1. коллекция только для чтения, переданная в метод, не должна изменяться снаружи пока этот метод не вернёт управление;
  2. коллекция только для чтения, которую вернул некоторый метод, должна изменяться владельцем только в очевидных случаях.
Попробуйте ImmutableArray
Вообще-то, Enumerable это ни в коем случае не лист и не коллекция; ставить меж ними знак равенства — верная дорога к интересным багам
Например,
IEnumerable<int> getEnumerable()
        {
            for (var rand = new Random();;)
            {
                yield return rand.Next();
            }
        }


Попробуйте вызвать getEnumerable().ToList() :))
НЛО прилетело и опубликовало эту надпись здесь
НЛО прилетело и опубликовало эту надпись здесь

Совершенно не аргумент. Не забывайте, что пустая коллекция это все же коллекция.. тоесть у "чего-то" есть ноль собственных или дочерних элементов, но сама коллекция все же есть. Если это "что-то" само равно null, то возращать пустую коллекцию будет неправильно.. Так что возвращать null или пустую коллекцию - это скорее вопрос поведения и логики.

PS. Случайно наткнулся и не смог пройти мимо)

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

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории