Pull to refresh

Comments 34

Первый тест абсолютно некорректен.
1. Метод Array() — не аналог Range и его результаты отличаются от Iterator() при одинаковых входных данных. В том числе и длиной конечного массива.
2. Скорость Iterator() сильно страдает за счет ToArray(), который будет создавать массивы длиной 4, 8 16, ..., 128 элементов, и потом в конце еще один длиной 90. Если уж замерять «цикл vs. итератор», то хотя бы сумму элементов считать.
Спасибо за замечание, исправил.
Что касается сути теста, то мне хотелось продемонстрировать паттерн, который я постоянно встречаю в коде: разработчик, получив IEnumerable, тут же вызывает ToList() или ToArray(). И я с вами полностью согласен, если сравнивать «цикл vs. итератор», этот тест некорректен.
Да, паттерн распространенный, но получается, что сравнивается не «работа yield в сравнении с реализацией „в лоб“, а скорость создания одного массива со скоростью создания и копирования семи, т.е. „manual vs. LINQ“.
Возможно, то, во что разворачивается yield, в этом эксперименте укладывается в 1% от общего времени выполнения, а 99% съедает создание массива. Хотя, конечно, я не проверял и думаю, что разница все равно будет в разы.
Применять же yield целесобразно в случаях обработки длинных последовательностей, когда каждое вычисление коллекции приводит к аллокации больших массивов памяти.

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

Я что-то упустил или в сгенерированном коде отсутствует инициализация последовательности?
Все правильно, последовательность генерируется по мере запрашивания элементов через MoveNext().
так, а где указание первого элемента? Там же с 10 должно быть вроде?

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


Пример кода
var iterator = new CompilerGeneratedYield(-2);
iterator.startingWith = startingWith;
return iterator;

private sealed class CompilerGeneratedYield // : ...iterfaces
{
    public int startingWith;
    // ...generated code
}
Вот. Теперь вроде все сходится :)
А чем разбирали сборку? У меня dotPeek и что-то он не показывает compiler-generated код…
обновился — заработало.

Тот же dotPeek. В Assembly Explorer'е, в контектном меню, Decompiled Sources.

Поздравляю, вы открыли для себя Корутины. Хотя Корутины это куда более глубокое понятие. И yield это оператор для создания генераторов.
А я вот не уловил, почему о yield говорится в контексте C# 7? Оно же вроде доступно со времен царя Гороха.
Eyecatcher же :). Да и нет там контекста: «Скоро выходит аж C# 7, но некоторые все еще избегают использования старых фич, об одной такой фиче и поговорим».
Хотелось бы понять в чем собственно преимущество возврата yeild return перед IEnumerable или любой другой коллекцией?
Как минимум, у вас не хранится в памяти вся коллекция. Вы получаете порции данных через IEnumerator. Естественно, если вызвать ToList() и иже с ним, то тогда да… пользы мало.
Другими словами это должно дать выигрыш в производительности, если нам нужно вернуть большую по размерам коллекцию?
Выигрыш в потреблении памяти.
Во-первых, генерируемая коллекция может быть бесконечной. Во-вторых, функция с yield return это сопрограмма, и компилятор сгенерит её конечный автомат за вас.

Моё мнение, что после появления Linq оператор yield больше не нужен.


Всё, что может сделать yield, также может сделать Linq, с сохранением всех плюшек типа ленивости вычислений.


Пример из статьи можно переписать одной строчкой.


Func<IEnumerable<int>> GetOddNumbers = () => Enumerable.Range(0, int.MaxValue).Where(x => x % 2 == 0);

Заодно сделаем функцию конечной до int.MaxValue, а то у вас в примере если в checked всё не обернуть, то тогда после переполнения int отрицательные числа начнут возвращаться.
По мне пусть лучше функция будет конечной чем начнёт отрицательные числа возвращать или кидать Arithmetic overflow.


Я рекомендую в новом коде не использовать yield совсем, в 2016 году это как бы old style C#.

Можете реализовать построчное чтение большого текстового файла с O(1) по памяти без дополнительных библиотек?

Уже всё написано до нас: File.ReadLines — прекрасно работает, возвращает итератор, по итератору можно делать Skip/Take/ToList и радоваться.


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

Уже всё написано до нас

Все?
А если надо читать из стрима?
Или из базы данных?
Или нужен обход дерева?


Если вы хотите меня опровергнуть

Это совершенно излишне. Гораздо интереснее, как вы сумеете написать генератор без yield там, где нет готового.

лучше вы приведите пример кода, который написан с использованием yield и который невозможно переписать с использованием linq без потери функциональности.

… и читабельности. Легко.


public IEnumerable<Identity> GetIdentitiesForCurrentUser()
{
  yield return TryAuthenticateViaProviderA(Context);
  yield return TryAuthenticateViaProviderB(Context);
  yield return TryAuthenticateLegacy(Context);
  yield return Identity.Anonymous;
}

var identity = GetIdentitiesForCurrentUser().First(u => u != null);

Ну или любой пример с валидацией через IEnumerable<ValidationError>.


Собственно, ваша ошибка в том, что вы считаете, что yield как-то заменяет/заменяется LINQ, хотя на самом деле, yield нужен тогда, когда вам нужно породить перечисление (enumeration) и лень/неэффективно/нечитабельно писать свой IEnumerator.

Спасибо за пример.


Тут классический случай когда мы правы оба.


Ваш пример вполне можно переписать без yield через несколько .Concat().


return Enumerable.Empty<Identity>().Concat(new[]{Provider1}).Concat(new [] {Provider2});

Но я соглашусь с вами про читабельность — конкретно в этом случае она хромает.


Любая истина рождается в споре, поэтому я подкорретирую свои слова:


Если работаете с потенциально бесконечными последовательностями — только linq и никакого yield.
Если работаете с конечными, но ленивыми последовательностями (что само по себе редкий кейс), то да, yield может выглядеть читабельнее.


P.S. В вашем примере лучше всего изменить дизайн, чтобы методы были с сигнатурой вида


bool TryGetProviderAIdentity(out Identity identity)
bool TryGetProviderBIdentity(out Identity identity)

И через if(Try1)...elseif(Try2) написать все вообще без yield и проверок на null.
Будет выглядеть просто и понятно.

Только до этого аутентификация шла через все провайдеры по-очереди, до первой успешной. А в вашей реализации — пройдут все сразу.

Как бы вы сделали генерацию бесконечной последовательности случайных чисел только на linq? Чтобы можно было написать, например, RandomNumbers().Take(1000)
Ваш пример вполне можно переписать без yield через несколько .Concat().

return Enumerable.Empty<Identity>().Concat(new[]{Provider1}).Concat(new [] {Provider2});

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


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


Если работаете с потенциально бесконечными последовательностями — только linq и никакого yield.

Тоже неправда. Я мог бы в конец своего примера с GetIdentity вставить


//в C# до сих пор нет yield!
foreach(var identity from GetLegacyIndentities()) yield return identity; 

и получить потенциально бесконечную последовательность.


В вашем примере лучше всего изменить дизайн, чтобы методы были с сигнатурой вида [...] и через if(Try1)...elseif(Try2)

Нет, не лучше. Во-первых, иногда мне нужны все (успешные) identity. Во-вторых, для большого количества провайдеров код с if заведомо менее читаем (собственно, он там и был изначально, я его рефакторил).

lair, INC_R, спасибо что потратили на меня время.

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

Linq нужен для трансформаций последовательностей (map,reduce), а вот как ты последовательность нагенерируешь — это не ответственность linq, для этого как раз yield хорошо подходит.
Уже всё написано до нас: File.ReadLines — прекрасно работает, возвращает итератор

Который внутри через yield написан, к слову.

Неправда, внутри он написан не через yield а через честную имплементацию IEnumerable, заимствованную из кода linq, в коде в комментариях явно написано "borrowed from Linq"


ссылка на ReferenceSource

А вот "borrowed from Linq" я там что-то не вижу.

А, нашел:


Abstract Iterator, borrowed from Linq. Used in anticipation of need for similar enumerables in the future

Обратите внимание, что класс-то internal, и если мне понадобится написать что-то аналогичное, опять придется "заимствовать". Честное слово, yield дешевле.

Да, в 4.6 переписали, похоже. В 4.5 был yield
Only those users with full accounts are able to leave comments. Log in, please.