реализация отчета о доходах - PullRequest
7 голосов
/ 13 апреля 2009

Я хочу знать все об операторе yield в простой для понимания форме.

Я читал об операторе yield и его простоте при реализации шаблона итератора. Однако большая часть его очень сухая. Я хотел бы получить под прикрытием и посмотреть, как Microsoft обрабатывает доходность.

Кроме того, когда вы используете разрыв доходности?

Ответы [ 4 ]

11 голосов
/ 13 апреля 2009

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

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

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

9 голосов
/ 13 апреля 2009

Как говорит Мехрдад, он создает конечный автомат.

Помимо использования Reflector (еще одно отличное предложение), вы можете найти мою статью о реализации блока итераторов . Было бы относительно просто, если бы не было finally блоков - но они вносят целый дополнительный аспект сложности!

3 голосов
/ 12 января 2019

Давайте немного перемотаем: ключевое слово yield переведено, как многие другие говорили конечному автомату.

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

A (конечный) конечный автомат - это просто кусок кода, который в зависимости от того, где вы находитесь в коде (в зависимости от предыдущего состояния, ввода), переходит к другому действию состояния, и это в значительной степени что происходит, когда вы используете метод и возвращаете метод с типом возврата IEnumerator<T> / IEnumerator. Ключевое слово yield - это то, что создает другое действие для перехода к следующему состоянию из предыдущего, поэтому управление состоянием создается в реализации MoveNext().

Именно это и собирается делать компилятор C # / Roslyn: проверить наличие ключевого слова yield плюс тип возвращаемого значения содержащего метода, будь то IEnumerator<T>, IEnumerable<T>, IEnumerator или IEnumerable, а затем создайте закрытый класс, отражающий этот метод, объединяющий необходимые переменные и состояния.

Если вам интересны подробности того, как конечный автомат и как итерации переписываются компилятором, вы можете проверить эти ссылки на Github:

Викторины 1 : AsyncRewriter (используется при написании кода async / await также наследуется от StateMachineRewriter, поскольку он также использует конечный автомат позади.

Как уже упоминалось, конечный автомат в значительной степени отражен в сгенерированной реализации bool MoveNext(), в которой есть switch + иногда некоторый старомодный goto, основанный на поле состояний, которое представляет различные пути выполнения для различных состояний в вашем методе.

Код, сгенерированный компилятором из пользовательского кода, выглядит не очень хорошо, в основном потому, что компилятор тут и там добавляет странные префиксы и суффиксы

Например, код:

public class TestClass 
{
    private int _iAmAHere = 0;

    public IEnumerator<int> DoSomething()
    {
        var start = 1;
        var stop = 42;
        var breakCondition = 34;
        var exceptionCondition = 41;
        var multiplier = 2;
        // Rest of the code... with some yield keywords somewhere below...

Переменные и типы, связанные с этим фрагментом кода выше, после компиляции будут выглядеть так:

public class TestClass
{
    [CompilerGenerated]
    private sealed class <DoSomething>d__1 : IEnumerator<int>, IDisposable, IEnumerator
    {
        // Always present
        private int <>1__state;
        private int <>2__current;

        // Containing class
        public TestClass <>4__this;

        private int <start>5__1;
        private int <stop>5__2;
        private int <breakCondition>5__3;
        private int <exceptionCondition>5__4;
        private int <multiplier>5__5;

Что касается самого конечного автомата, давайте рассмотрим очень простой пример с фиктивным ветвлением для получения некоторого чётного / нечетного материала.

public class Example
{
    public IEnumerator<string> DoSomething()
    {
        const int start = 1;
        const int stop = 42;

        for (var index = start; index < stop; index++)
        {
            yield return index % 2 == 0 ? "even" : "odd";
        }
    }
} 

будет переведено в MoveNext как:

private bool MoveNext()
{
    switch (<>1__state)
    {
        default:
            return false;
        case 0:
            <>1__state = -1;
            <start>5__1 = 1;
            <stop>5__2 = 42;
            <index>5__3 = <start>5__1;
            break;
        case 1:
            <>1__state = -1;
            goto IL_0094;
        case 2:
            {
                <>1__state = -1;
                goto IL_0094;
            }
            IL_0094:
            <index>5__3++;
            break;
    }
    if (<index>5__3 < <stop>5__2)
    {
        if (<index>5__3 % 2 == 0)
        {
            <>2__current = "even";
            <>1__state = 1;
            return true;
        }
        <>2__current = "odd";
        <>1__state = 2;
        return true;
    }
    return false;
} 

Как вы можете видеть, эта реализация далеко не проста, но она делает свою работу!

Общая информация 2 : Что происходит с типом возврата метода IEnumerable / IEnumerable<T>?
Что ж, вместо того, чтобы просто генерировать класс, реализующий IEnumerator<T>, он будет генерировать класс, который реализует как IEnumerable<T>, так и IEnumerator<T>, так что реализация IEnumerator<T> GetEnumerator() будет использовать тот же сгенерированный класс.

Горячее напоминание о нескольких интерфейсах, которые реализуются автоматически при использовании ключевого слова yield:

public interface IEnumerable<out T> : IEnumerable
{
    new IEnumerator<T> GetEnumerator();
}

public interface IEnumerator<out T> : IDisposable, IEnumerator
{
    T Current { get; }
}

public interface IEnumerator
{
    bool MoveNext();

    object Current { get; }

    void Reset();
}

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

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

О второй части вопроса, т. Е. yield break, на него ответили здесь

Указывает, что итератор подошел к концу. Вы можете думать о yield break в качестве оператора возврата, который не возвращает значение.

...