Когда НЕ использовать yield (return)

На этот вопрос уже есть ответ здесь:
Есть ли причина не использовать "возвращение дохода" при возврате IEnumerable?

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

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

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

11 ответов

Решение

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

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

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) в пространстве кучи, потому что каждый перечислитель размещен в куче. (О реализациях C#, о которых я знаю; соответствующая реализация может иметь другие характеристики стека или пространства кучи.)

Но повторение дерева может быть 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

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

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

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

    // 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;
    }
    
  • Избегайте использования yield return, если вы не хотите откладывать выполнение кода для метода. Пример:

    // 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);
    }
    

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

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

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

Здесь много отличных ответов. Я бы добавил это: не используйте yield 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>();
}

Обновить

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

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

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

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

Обновление 2

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

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

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

Если вы выбираете между "доходность возврата" и List<T> при написании метода учитывайте: стоит ли вычислять каждый отдельный элемент, и нужно ли вызывающей стороне перечислять результаты более одного раза? Если вы знаете, что ответы да и да, вы не должны использовать yield return (если, например, созданный список не является очень большим, и вы не можете позволить себе использовать память, которую он будет использовать. Помните, еще одно преимущество 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 может вызвать проблемы всякий раз, когда вызывающий объект изменяет что-то, что, как предполагает функция выдачи, не изменяется.

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

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

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

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

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

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

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

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

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

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

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

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

Другие вопросы по тегам