Поиск…


Вступление

Когда вы используете ключевое слово yield в инструкции, вы указываете, что метод, оператор или get accessor, в котором он отображается, является итератором. Использование yield для определения итератора устраняет необходимость в явном дополнительном классе (класс, который содержит состояние для перечисления) при реализации шаблона IEnumerable и IEnumerator для пользовательского типа коллекции.

Синтаксис

  • return return [TYPE]
  • разрыв дохода

замечания

Полагая ключевое слово yield в методе с типом возвращаемого значения IEnumerable , IEnumerable<T> , IEnumerator или IEnumerator<T> сообщает компилятору сгенерировать реализацию возвращаемого типа ( IEnumerable или IEnumerator ), который при циклическом запуске метод до каждого «урожая», чтобы получить каждый результат.

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

yield break также может быть использован для прекращения последовательности в любое время.

Поскольку ключевому слову yield требуется тип интерфейса итератора в качестве возвращаемого типа, например IEnumerable<T> , вы не можете использовать его в методе async, так как это возвращает объект Task<IEnumerable<T>> .

дальнейшее чтение

Простое использование

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

В следующем примере есть оператор возврата доходности, который находится внутри цикла for .

public static IEnumerable<int> Count(int start, int count)
{
    for (int i = 0; i <= count; i++)
    {
        yield return start + i;
    }
}

Тогда вы можете назвать это:

foreach (int value in Count(start: 4, count: 10))
{
    Console.WriteLine(value);
}

Консольный выход

4
5
6
...
14

Живая демонстрация на .NET скрипке

Каждая итерация тела оператора foreach создает вызов функции итератора Count . Каждый вызов функции итератора переходит к следующему исполнению оператора yield return , который возникает во время следующей итерации цикла for .

Более подходящее использование

public IEnumerable<User> SelectUsers()
{
    // Execute an SQL query on a database.
    using (IDataReader reader = this.Database.ExecuteReader(CommandType.Text, "SELECT Id, Name FROM Users"))
    {
        while (reader.Read())
        {
            int id = reader.GetInt32(0);
            string name = reader.GetString(1);
            yield return new User(id, name);
        }
    }
}

Конечно, есть другие способы получить IEnumerable<User> из базы данных SQL - это просто демонстрирует, что вы можете использовать yield чтобы превратить все, что имеет семантику «последовательность элементов» в IEnumerable<T> которую кто-то может перебирать через ,

Раннее прекращение

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

public static IEnumerable<int> CountUntilAny(int start, HashSet<int> earlyTerminationSet)
{
    int curr = start;

    while (true)
    {
        if (earlyTerminationSet.Contains(curr))
        {
            // we've hit one of the ending values
            yield break;
        }

        yield return curr;

        if (curr == Int32.MaxValue)
        {
            // don't overflow if we get all the way to the end; just stop
            yield break;
        }

        curr++;
    }
}

Вышеуказанный метод будет перебирать из заданной start позиции до тех пор, пока не будет earlyTerminationSet одно из значений в earlyTerminationSet .

// Iterate from a starting point until you encounter any elements defined as 
// terminating elements
var terminatingElements = new HashSet<int>{ 7, 9, 11 };
// This will iterate from 1 until one of the terminating elements is encountered (7)
foreach(var x in CountUntilAny(1,terminatingElements))
{
    // This will write out the results from 1 until 7 (which will trigger terminating)
    Console.WriteLine(x);
}

Выход:

1
2
3
4
5
6

Живая демонстрация на .NET скрипке

Правильно проверяющие аргументы

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

public static IEnumerable<int> Count(int start, int count)
{
    // The exception will throw when the method is called, not when the result is iterated
    if (count < 0)
        throw new ArgumentOutOfRangeException(nameof(count));

    return CountCore(start, count);
}

private static IEnumerable<int> CountCore(int start, int count)
{
    // If the exception was thrown here it would be raised during the first MoveNext()
    // call on the IEnumerator, potentially at a point in the code far away from where
    // an incorrect value was passed.
    for (int i = 0; i < count; i++)
    {
        yield return start + i;
    }
}

Вызов бокового кода (использование):

// Get the count
var count = Count(1,10);
// Iterate the results
foreach(var x in count)
{
    Console.WriteLine(x);
}

Выход:

1
2
3
4
5
6
7
8
9
10

Живая демонстрация на .NET скрипке

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

Это означает, что вы не узнаете о недопустимых аргументах (передаете null и т. Д.) При первом вызове метода (поскольку это создает конечный автомат), только когда вы пытаетесь получить доступ к первому элементу (потому что только тогда код внутри метод get запускается конечным автоматом). Обернув его обычным методом, который сначала проверяет аргументы, вы можете проверить их при вызове метода. Это пример неудачи.

При использовании C # 7+ функция CountCore может быть удобно скрыта в функции Count как локальная функция . См. Пример здесь .

Возвращает другой Перечислимый в методе, возвращающем Enumerable

public IEnumerable<int> F1()
{
    for (int i = 0; i < 3; i++)
        yield return i;

    //return F2(); // Compile Error!!
    foreach (var element in F2())
        yield return element;
}

public int[] F2()
{
    return new[] { 3, 4, 5 };
}

Ленивая оценка

Только когда оператор foreach переходит к следующему элементу, блок итератора оценивает до следующего оператора yield .

Рассмотрим следующий пример:

private IEnumerable<int> Integers()
{
    var i = 0;
    while(true)
    {
        Console.WriteLine("Inside iterator: " + i);
        yield return i;
        i++;
    }
}

private void PrintNumbers()
{
    var numbers = Integers().Take(3);
    Console.WriteLine("Starting iteration");

    foreach(var number in numbers)
    {
        Console.WriteLine("Inside foreach: " + number);
    }
}

Это приведет к выводу:

Запуск итерации
Внутри итератора: 0
Внутри foreach: 0
Внутри итератора: 1
Внутри foreach: 1
Внутри итератора: 2
Внутри foreach: 2

Посмотреть демо

Как следствие:

  • «Начальная итерация» сначала печатается, даже если метод итератора был вызван до того, как строка напечатала его, потому что строка Integers().Take(3); на самом деле не запускает итерацию (не был IEnumerator.MoveNext() вызов IEnumerator.MoveNext() )
  • Линии, печатающие на консоли, чередуются между тем, который находится внутри метода итератора, и внутри внутри foreach , а не все внутри метода итератора, оценивая сначала
  • Эта программа завершается из-за метода .Take() , хотя метод итератора имеет значение while true которое никогда не прерывается.

Попробуйте ... наконец-то

Если метод итератора имеет выход внутри try...finally , возвращаемый IEnumerator выполнит оператор finally когда Dispose вызывается на нем, если текущая точка оценки находится внутри блока try .

С учетом функции:

private IEnumerable<int> Numbers()
{
    yield return 1;
    try
    {
        yield return 2;
        yield return 3;
    }
    finally
    {
        Console.WriteLine("Finally executed");
    }
}

При звонке:

private void DisposeOutsideTry()
{
    var enumerator = Numbers().GetEnumerator();

    enumerator.MoveNext();
    Console.WriteLine(enumerator.Current);
    enumerator.Dispose();
}

Затем он печатает:

1

Посмотреть демо

При звонке:

private void DisposeInsideTry()
{
    var enumerator = Numbers().GetEnumerator();

    enumerator.MoveNext();
    Console.WriteLine(enumerator.Current);
    enumerator.MoveNext();
    Console.WriteLine(enumerator.Current);
    enumerator.Dispose();
}

Затем он печатает:

1
2
Наконец выполнен

Посмотреть демо

Использование yield для создания IEnumerator при внедрении IEnumerable

Интерфейс IEnumerable<T> имеет единственный метод GetEnumerator() , который возвращает IEnumerator<T> .

Хотя ключевое слово yield можно использовать для непосредственного создания IEnumerable<T> , его также можно использовать точно так же, чтобы создать IEnumerator<T> . Единственное, что изменяется, это тип возвращаемого метода.

Это может быть полезно, если мы хотим создать собственный класс, который реализует IEnumerable<T> :

public class PrintingEnumerable<T> : IEnumerable<T>
{
    private IEnumerable<T> _wrapped;

    public PrintingEnumerable(IEnumerable<T> wrapped)
    {
        _wrapped = wrapped;
    }

    // This method returns an IEnumerator<T>, rather than an IEnumerable<T>
    // But the yield syntax and usage is identical.
    public IEnumerator<T> GetEnumerator()
    {
        foreach(var item in _wrapped)
        {
            Console.WriteLine("Yielding: " + item);
            yield return item;
        }
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }
}

(Обратите внимание, что этот конкретный пример является просто иллюстративным и может быть более чисто реализован с помощью одного метода итератора, возвращающего IEnumerable<T> .)

Простая оценка

Ключевое слово yield позволяет ленивую оценку коллекции. Принудительная загрузка всей коллекции в память называется нетерпеливой оценкой .

Следующий код показывает это:

IEnumerable<int> myMethod()
{
    for(int i=0; i <= 8675309; i++)
    {
        yield return i;
    }
}
...
// define the iterator
var it = myMethod.Take(3);
// force its immediate evaluation
// list will contain 0, 1, 2
var list = it.ToList();

Вызов ToList , ToDictionary или ToArray заставит немедленную оценку перечисления, извлекая все элементы в коллекцию.

Ленивый пример оценки: номера Фибоначчи

using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics; // also add reference to System.Numberics

namespace ConsoleApplication33
{
    class Program
    {
        private static IEnumerable<BigInteger> Fibonacci()
        {
            BigInteger prev = 0;
            BigInteger current = 1;
            while (true)
            {
                yield return current;
                var next = prev + current;
                prev = current;
                current = next;
            }
        }

        static void Main()
        {
            // print Fibonacci numbers from 10001 to 10010
            var numbers = Fibonacci().Skip(10000).Take(10).ToArray();
            Console.WriteLine(string.Join(Environment.NewLine, numbers));
        }
    }
}

Как это работает под капотом (я рекомендую декомпилировать полученный файл .exe в инструменте IL Disaambler):

  1. Компилятор C # генерирует класс, реализующий IEnumerable<BigInteger> и IEnumerator<BigInteger> ( <Fibonacci>d__0 in ildasm).
  2. Этот класс реализует конечный автомат. Состояние состоит из текущей позиции в методе и значениях локальных переменных.
  3. Самый интересный код - в методе bool IEnumerator.MoveNext() . В основном, что MoveNext() делает:
    • Восстанавливает текущее состояние. Переменные типа prev и current становятся полями в нашем классе ( <current>5__2 и <prev>5__1 in ildasm). В нашем методе мы имеем две позиции ( <>1__state ): сначала в открывающей фигурной скобке, второй - при yield return .
    • Выполняет код до следующего yield return yield break или yield break / } .
    • Для yield return результата yield return значение сохраняется, поэтому Current свойство может вернуть его. true возвращается. В этот момент текущее состояние сохраняется снова для следующего вызова MoveNext .
    • Для метода yield break / } метод возвращает false итерация выполняется.

Также обратите внимание, что 10001-е число составляет 468 байт. State machine сохраняет только current и prev переменные как поля. Хотя если мы хотим сохранить все числа в последовательности от первой до 10000-й, объем потребляемой памяти будет превышать 4 мегабайта. Такая ленивая оценка, если она правильно используется, в некоторых случаях может уменьшить объем памяти.

Разница между разрывом и выходом

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

Запутанная часть состоит в том, что оба ключевых слова (или ключевые фразы) имеют смысл только внутри циклов ( foreach , while ...). Когда выбирать один за другим?

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

Давайте посмотрим несколько примеров:

    /// <summary>
    /// Yields numbers from 0 to 9
    /// </summary>
    /// <returns>{0,1,2,3,4,5,6,7,8,9}</returns>
    public static IEnumerable<int> YieldBreak()
    {
        for (int i = 0; ; i++)
        {
            if (i < 10)
            {
                // Yields a number
                yield return i;
            }
            else
            {
                // Indicates that the iteration has ended, everything 
                // from this line on will be ignored
                yield break;
            }
        }
        yield return 10; // This will never get executed
    }
    /// <summary>
    /// Yields numbers from 0 to 10
    /// </summary>
    /// <returns>{0,1,2,3,4,5,6,7,8,9,10}</returns>
    public static IEnumerable<int> Break()
    {
        for (int i = 0; ; i++)
        {
            if (i < 10)
            {
                // Yields a number
                yield return i;
            }
            else
            {
                // Terminates just the loop
                break;
            }
        }
        // Execution continues
        yield return 10;
    }


Modified text is an extract of the original Stack Overflow Documentation
Лицензировано согласно CC BY-SA 3.0
Не связан с Stack Overflow