Когда не использовать yield (return) [дубликат]

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

здесь есть несколько полезных вопросов о преимуществах 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

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


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

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

  • избегайте использования 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;
    }
}

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

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

обновление

вот результаты мой тест:

Benchmark Results

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

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

обновление 2

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


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

например, LINQ-to-objects построен на "yield return". Если вы написали медленный запрос 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 может вызвать проблемы, когда вызывающий абонент изменяет что-то, что функция yielding предполагает, что не изменяется.


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


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


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

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

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

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

Я должен поддерживать кучу кода от парня, который был абсолютно одержим возврата и 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.