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


этот вопрос уже есть ответ здесь:
есть ли когда-нибудь причина не использовать 'yield return' при возврате IEnumerable?

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

  • может кто-то прояснить для себя выход ключевое слово

  • интересное использование C# yield
    ключевое слово

  • Что такое доходность сайта

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

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

11 145

11 ответов:

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

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

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" на внешний итератор, тогда будет о(ч) вложенных вызовов метода MoveNext. Поскольку он делает это O(n) раз для дерева с n элементами, это делает алгоритм O(hn). А поскольку высота бинарного дерева является LG п

но итерация дерева может быть 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 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 вместе, не оказывая отрицательного влияния на производительность на каждом шаге (aka отложенное выполнение). Альтернативная картина будет положить 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.