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

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

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

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 клиент elements-если вы просто прочитаете этот файл в память, это потребует сохранить все из них в памяти одновременно, даже если они обрабатываются последовательно. Таким образом, вы можете использовать итераторы для прохождения коллекции один за другим. В этом случае вы должны потратить только память на один элемент.

Как оказалось, используя 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...
}

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

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

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


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