Какова цель / преимущество использования итераторов yield return в C#?


все примеры, которые я видел использовать yield return x; в C# метод может быть сделано таким же образом, просто вернув весь список. В тех случаях, есть ли преимущество в использовании yield return синтаксис против возврата списка?

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

10 71

10 ответов:

но что, если бы вы сами собирали коллекцию?

В общем, итераторы могут быть использованы для лениво генерировать последовательность объектов. Например Enumerable.Range метод не имеет какой-либо коллекции внутренне. Он просто генерирует следующее число по требованию. Существует много применений для этого ленивого поколения последовательности с использованием конечного автомата. Большинство из них покрытыконцепции функционального программирования.

по-моему, если вы рассматриваете итераторы просто как способ перечисления через коллекцию (это всего лишь один из самых простых случаев использования), вы идете по неправильному пути. Как я уже сказал, итераторы-это средства для возврата последовательностей. Последовательность может быть даже бесконечный. Не было бы никакого способа вернуть список с бесконечной длиной и использовать первые 100 элементов. Это и иногда лениться. возврат коллекции значительно отличается от возврата генератора коллекции (что и есть итератор). Это сравнение яблок с апельсинами.

гипотетический пример:

static IEnumerable<int> GetPrimeNumbers() {
   for (int num = 2; ; ++num) 
       if (IsPrime(num))
           yield return num;
}

static void Main() { 
   foreach (var i in GetPrimeNumbers()) 
       if (i < 10000)
           Console.WriteLine(i);
       else
           break;
}

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

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

но что, если у вас нет списка?

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

public IEnumerable<T> InOrder()
{
    foreach (T k in kids)
        foreach (T n in k.InOrder())
            yield return n;
    yield return (T) this;
}

public IEnumerable<T> PreOrder()
{
    yield return (T) this;
    foreach (T k in kids)
        foreach (T n in k.PreOrder())
            yield return n;
}

Ленивая Оценка / Отложенное Выполнение

блоки итератора "yield return" не будут выполняться любой кода, пока вы на самом деле не вызовете этот конкретный результат. Это означает, что они также могут быть скованы вместе эффективно. Pop quiz: предполагая, что функция "ReadLines ()" считывает все строки из текстового файла и реализуется с помощью блока итератора, сколько раз следующий код будет повторяться над файлом?

var query = ReadLines(@"C:\MyFile.txt")
                            .Where(l => l.Contains("search text") )
                            .Select(l => int.Parse(l.SubString(5,8))
                            .Where(i => i > 10 );

int sum=0;
foreach (int value in query) 
{
    sum += value;
}

ответ только один, и что не до пути вниз в foreach петли.

разделение

снова используя гипотетический

иногда последовательности, которые вам нужно вернуть, слишком велики, чтобы поместиться в памяти. Например, около 3 месяцев назад я принял участие в проекте по миграции данных между базами данных MS SLQ. Данные экспортировались в формате XML. возврата оказалось весьма полезным с XmlReader. Это значительно упростило Программирование. Например, предположим, что файл имеет 1000 клиент элементы-если вы просто читаете этот файл в память, это потребует, чтобы сохранить все из них в памяти одновременно, даже если они обрабатываются последовательно. Таким образом, вы можете использовать итераторы для прохождения коллекции один за другим. В этом случае вы должны потратить только память на один элемент.

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

в игрушечных / демонстрационных сценариях нет большой разницы. Но есть ситуации, когда итераторы с выходом полезны - иногда весь список недоступен (например, потоки), или список является вычислительно дорогим и вряд ли понадобится в полном объеме.

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

взгляните на эту дискуссию в блоге Эрика Уайта (отличный блог, кстати) на ленивый против нетерпеливой оценки.

С помощью yield return вы можете перебирать элементы без необходимости создавать список. Если вам не нужен список, но вы хотите перебирать некоторые элементы, это может быть проще написать

foreach (var foo in GetSomeFoos()) {
    operate on foo
}

чем

foreach (var foo in AllFoos) {
    if (some case where we do want to operate on foo) {
        operate on foo
    } else if (another case) {
        operate on foo
    }
}

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

вот мой предыдущий принятый ответ на точно такой же вопрос:

Yield ключевое слово добавленная стоимость?

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

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

но это создает проблемы для моих пользователей. Они должны реализовать интерфейс обработчика и поэтому они должны написать класс конечного автомата, который отвечает на методы обратного вызова. Это трудно сделать правильно, поэтому проще всего использовать реализацию запаса, которая строит дерево DOM, и тогда они будут иметь возможность ходить по дереву. Но тогда вся структура буферизуется в памяти-нехорошо.

но как насчет того, чтобы вместо этого я написал свой парсер как метод итератора?

IEnumerable<LanguageElement> Parse(Stream stream)
{
    // imperative code that pulls from the stream and occasionally 
    // does things like:

    yield return new BeginStatement("if");

    // and so on...
}

это будет не сложнее написать, чем подход callback-interface-просто верните объект, полученный из my LanguageElement базовый класс вместо вызова метода обратного вызова.

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

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

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