Когда НЕ использовать доходность (возврат)

У этого вопроса уже есть ответ:
Есть ли причина, по которой не использовать return return при возврате IEnumerable?

Здесь есть несколько полезных вопросов о преимуществах yield return. Например,

Я ищу мысли, когда НЕ использовать yield return. Например, если я ожидаю, что вам нужно вернуть все элементы в коллекции, это не похоже, что yield будет полезен, не так ли?

Каковы случаи, когда использование yield будет ограничивать, излишне, затруднять мне работу, или иначе следует избегать?

Ответ 1

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

Хорошая идея тщательно подумать о том, как вы используете "доходность доходности" при работе с рекурсивно определенными структурами. Например, я часто вижу это:

public static IEnumerable<T> PreorderTraversal<T>(Tree<T> root)
{
    if (root == null) yield break;
    yield return root.Value;
    foreach(T item in PreorderTraversal(root.Left))
        yield return item;
    foreach(T item in PreorderTraversal(root.Right))
        yield return item;
}

Совершенно разумный код, но он имеет проблемы с производительностью. Предположим, что дерево h глубокое. Тогда в большинстве точек будет O (h) встроенных итераторов. Вызов "MoveNext" на внешний итератор затем сделает O (h) вложенные вызовы MoveNext. Так как это делает O (n) раз для дерева с n элементами, это делает алгоритм O (hn). А так как высота двоичного дерева равна lg n <= h <= n, это означает, что алгоритм в лучшем случае O (n lg n) и в худшем случае O (n ^ 2) по времени, а лучший случай O (lg n) и худший случай O (n) в пространстве стека. Это O (h) в куче пространстве, потому что каждый перечислитель выделяется в куче. (О реализациях С#, о которых я знаю, соответствующая реализация может иметь другие характеристики стека или кучи).

Но повторение дерева может быть O (n) во времени и O (1) в пространстве стека. Вы можете написать это вместо:

public static IEnumerable<T> PreorderTraversal<T>(Tree<T> root)
{
    var stack = new Stack<Tree<T>>();
    stack.Push(root);
    while (stack.Count != 0)
    {
        var current = stack.Pop();
        if (current == null) continue;
        yield return current.Value;
        stack.Push(current.Left);
        stack.Push(current.Right);
    }
}

который по-прежнему использует возврат доходности, но гораздо умнее об этом. Теперь мы имеем O (n) во времени и O (h) в кучевом пространстве и O (1) в пространстве стека.

Дополнительная литература: см. статью Уэса Дайера по теме:

http://blogs.msdn.com/b/wesdyer/archive/2007/03/23/all-about-iterators.aspx

Ответ 2

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

Я могу вспомнить пару случаев, IE:

  • Избегайте использования возврата доходности при возврате существующего итератора. Пример:

    // Don't do this, it creates overhead for no reason
    // (a new state machine needs to be generated)
    public IEnumerable<string> GetKeys() 
    {
        foreach(string key in _someDictionary.Keys)
            yield return key;
    }
    // DO this
    public IEnumerable<string> GetKeys() 
    {
        return _someDictionary.Keys;
    }
    
  • Избегайте использования доходности возврата, если вы не хотите откладывать код выполнения для метода. Пример:

    // Don't do this, the exception won't get thrown until the iterator is
    // iterated, which can be very far away from this method invocation
    public IEnumerable<string> Foo(Bar baz) 
    {
        if (baz == null)
            throw new ArgumentNullException();
         yield ...
    }
    // DO this
    public IEnumerable<string> Foo(Bar baz) 
    {
        if (baz == null)
            throw new ArgumentNullException();
         return new BazIterator(baz);
    }
    

Ответ 3

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

Другими словами, если вам не нужна ленивая оценка последовательности, вы можете пропустить использование yield. Когда это будет? Это было бы тогда, когда вы не возражаете, чтобы вся ваша коллекция хранилась в памяти. В противном случае, если у вас есть огромная последовательность, которая негативно повлияет на память, вы бы хотели использовать yield для работы по ней шаг за шагом (т.е. Лениво). Профилировщик может пригодиться при сравнении обоих подходов.

Обратите внимание, что большинство операторов LINQ возвращают IEnumerable<T>. Это позволяет нам постоянно связывать различные операции LINQ вместе, не отрицательно влияя на производительность на каждом этапе (ака отсроченное выполнение). Альтернативным изображением будет вызов ToList() между каждой операцией LINQ. Это приведет к немедленному выполнению каждого предшествующего оператора LINQ перед выполнением следующего (связанного) оператора LINQ, тем самым оставив любую выгоду от ленивой оценки и используя IEnumerable<T> до необходимого.

Ответ 4

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

IEnumerable<UserRight> GetSuperUserRights() {
    if(SuperUsersAllowed) {
        yield return UserRight.Add;
        yield return UserRight.Edit;
        yield return UserRight.Remove;
    }
}

В этих случаях создание объекта Enumerator является более дорогостоящим и более подробным, чем просто создание структуры данных.

IEnumerable<UserRight> GetSuperUserRights() {
    return SuperUsersAllowed
           ? new[] {UserRight.Add, UserRight.Edit, UserRight.Remove}
           : Enumerable.Empty<UserRight>();
}

Update

Здесь результаты моего теста:

Результаты тестов

Эти результаты показывают, сколько времени потребовалось (в миллисекундах) для выполнения операции 1,000,000 раз. Меньшие числа лучше.

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

Ответ 5

Эрик Липперт поднимает хорошую точку (слишком плохо С# не имеет поток сглаживания, как Cw). Я бы добавил, что иногда процесс перечисления дорог по другим причинам, и поэтому вы должны использовать список, если вы собираетесь перебирать IEnumerable более одного раза.

Например, LINQ-to-objects построена на "yield return". Если вы написали медленный запрос LINQ (например, который фильтрует большой список в небольшой список или выполняет сортировку и группировку), может быть разумным вызвать ToList() в результате запроса, чтобы избежать перечисления нескольких times (который фактически выполняет запрос несколько раз).

Если вы выбираете между "yield return" и List<T> при написании метода, подумайте: это дорого, и вызывающий должен перечислить результаты более одного раза? Если вы знаете, что ответ "да", то не используйте "возврат доходности", если только созданный список не является чрезвычайно большим (и вы не можете позволить себе память, которую он будет использовать), помните, что еще одно преимущество yield заключается в том, что список результатов не обязательно должен быть полностью в памяти сразу).

Другая причина не использовать "доходность возврата" - это то, что операции чередования опасны. Например, если ваш метод выглядит примерно так,

IEnumerable<T> GetMyStuff() {
    foreach (var x in MyCollection)
        if (...)
            yield return (...);
}

это опасно, если есть вероятность, что MyCollection изменится из-за чего-то вызывающего:

foreach(T x in GetMyStuff()) {
    if (...)
        MyCollection.Add(...);
        // Oops, now GetMyStuff() will throw an exception because
        // MyCollection was modified.
}

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

Ответ 6

Выход будет ограниченным/ненужным, если вам нужен произвольный доступ. Если вам нужно получить доступ к элементу 0, то элемент 99, вы в значительной степени устранили полезность ленивой оценки.

Ответ 7

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

Ответ 8

Я бы избегал использования yield return, если у метода есть побочный эффект, который вы ожидаете при вызове метода. Это связано с отсроченным исполнением, которое упоминает Pop Catalin.

Один побочный эффект может быть изменен системой, что может произойти в методе типа IEnumerable<Foo> SetAllFoosToCompleteAndGetAllFoos(), который нарушает принцип единой ответственности. Это довольно очевидно (теперь...), но не столь очевидный побочный эффект может заключаться в создании кэшированного результата или подобного оптимизация.

Мои эмпирические правила (опять же, теперь...):

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

Ответ 9

Мне нужно сохранить кучу кода от парня, который был абсолютно одержим возвратом доходности и IEnumerable. Проблема в том, что многие сторонние API-интерфейсы, которые мы используем, а также много нашего собственного кода, зависят от списков или массивов. Поэтому мне приходится делать:

IEnumerable<foo> myFoos = getSomeFoos();
List<foo> fooList = new List<foo>(myFoos);
thirdPartyApi.DoStuffWithArray(fooList.ToArray());

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

Ответ 10

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

Ответ 11

Если вы определяете метод расширения Linq-y, в котором вы переносите фактические члены Linq, эти члены чаще всего возвращают итератор. Упрощение через этот итератор не является необходимым.

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