Нужно ли вызывать `yield break` после цикла?

2

В моем коде у меня есть такой метод

public static IEnumerable<int> GetDiff(int start, int end)
{
    while (start < end)
    {
        yield return start;
            start++;
    }
    yield break; // do we need to call it explicitly?
}

Итак, интересующие меня тестовые примеры - это GetDiff(1, 5) и GetDiff(5, 1). Хотя ясно, что происходит в первом случае, не совсем ясно, как это закончится во втором без yield break; после цикла

  • 3
    нет. кто тебя этому научил? что вы имеете в виду "как это закончить"? это просто идет до конца процедуры
  • 0
    yield break похож на поток, достигающий конца своей функции и заканчивающийся.
Показать ещё 5 комментариев
Теги:

3 ответа

6
Лучший ответ

Нет, это не обязательно. Это будет работать:

public static IEnumerable<int> GetDiff(int start, int end)
{
    while (start < end)
    {
        yield return start;
        start++;
    }
    // yield break; - It is not necessary. It is like 'return' which does not return a value.
}

В этом случае выполнение функции завершится простым выходом из нее.

Но вы можете написать так:

public static IEnumerable<int> GetDiff(int start, int end)
{
    while (true)
    {
        if (start >= end)
            yield break;
        yield return start;
        start++;
    }
    Console.WriteLine("Finish"); // note that this line will not be executed
}
  • 0
    Строка находится вне метода, поэтому она даже не будет компилироваться. Вы, вероятно, хотели поставить его после цикла for, верно?
  • 0
    @ Ghost4Man ты прав, я уже исправил
2

Выложил ваш код в компилятор, собрал и вернул обратно к С#, это получилось:

using System.Collections.Generic;

public static IEnumerable<int> GetDiff(int start, int end)
{
    while (start < end)
    {
        yield return start;
        start++;
    }
}

Я использовал LINQPad 5 и ILSpy для LINQPad.

Никакого перерыва здесь. Хорошо, хорошо, у которого все еще есть сахар... давайте переведем на С# 1.0. Вот как выглядит код:

using System.Collections.Generic;
using System.Runtime.CompilerServices;

[IteratorStateMachine(typeof(<GetDiff>d__1))]
public static IEnumerable<int> GetDiff(int start, int end)
{
    <GetDiff>d__1 <GetDiff>d__ = new <GetDiff>d__1(-2);
    <GetDiff>d__.<>3__start = start;
    <GetDiff>d__.<>3__end = end;
    return <GetDiff>d__;
}

Он создает экземпляр скрытого анонимного класса <GetDiff>d__1, устанавливает его start и end атрибуты и возвращает его. Мы вернемся к -2 который передаем его конструктору.

Ниже приведен тот же код выше, за исключением того, что в IL:

.method public hidebysig static 
    class [mscorlib]System.Collections.Generic.IEnumerable'1<int32> GetDiff (
        int32 start,
        int32 end
    ) cil managed 
{
    .custom instance void [mscorlib]System.Runtime.CompilerServices.IteratorStateMachineAttribute::.ctor(class [mscorlib]System.Type) = (
        01 00 17 55 73 65 72 51 75 65 72 79 2b 3c 47 65
        74 44 69 66 66 3e 64 5f 5f 31 00 00
    )
    // Method begins at RVA 0x2052
    // Code size 22 (0x16)
    .maxstack 8

    IL_0000: ldc.i4.s -2
    IL_0002: newobj instance void UserQuery/'<GetDiff>d__1'::.ctor(int32)
    IL_0007: dup
    IL_0008: ldarg.0
    IL_0009: stfld int32 UserQuery/'<GetDiff>d__1'::'<>3__start'
    IL_000e: dup
    IL_000f: ldarg.1
    IL_0010: stfld int32 UserQuery/'<GetDiff>d__1'::'<>3__end'
    IL_0015: ret
} // end of method UserQuery::GetDiff

Класс <GetDiff>d__1 выглядит так:

// <GetDiff>d__1
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.CompilerServices;

[CompilerGenerated]
private sealed class <GetDiff>d__1 : IEnumerable<int>, IEnumerable, IEnumerator<int>, IDisposable, IEnumerator
{
    private int <>1__state;

    private int <>2__current;

    private int <>l__initialThreadId;

    private int start;

    public int <>3__start;

    private int end;

    public int <>3__end;

    int IEnumerator<int>.Current
    {
        [DebuggerHidden]
        get
        {
            return <>2__current;
        }
    }

    object IEnumerator.Current
    {
        [DebuggerHidden]
        get
        {
            return <>2__current;
        }
    }

    [DebuggerHidden]
    public <GetDiff>d__1(int <>1__state)
    {
        this.<>1__state = <>1__state;
        <>l__initialThreadId = Environment.CurrentManagedThreadId;
    }

    [DebuggerHidden]
    void IDisposable.Dispose()
    {
    }

    private bool MoveNext()
    {
        switch (<>1__state)
        {
        default:
            return false;
        case 0:
            <>1__state = -1;
            break;
        case 1:
            <>1__state = -1;
            start++;
            break;
        }
        if (start < end)
        {
            <>2__current = start;
            <>1__state = 1;
            return true;
        }
        return false;
    }

    bool IEnumerator.MoveNext()
    {
        //ILSpy generated this explicit interface implementation from .override directive in MoveNext
        return this.MoveNext();
    }

    [DebuggerHidden]
    void IEnumerator.Reset()
    {
        throw new NotSupportedException();
    }

    [DebuggerHidden]
    IEnumerator<int> IEnumerable<int>.GetEnumerator()
    {
        <GetDiff>d__1 <GetDiff>d__;
        if (<>1__state == -2 && <>l__initialThreadId == Environment.CurrentManagedThreadId)
        {
            <>1__state = 0;
            <GetDiff>d__ = this;
        }
        else
        {
            <GetDiff>d__ = new <GetDiff>d__1(0);
        }
        <GetDiff>d__.start = <>3__start;
        <GetDiff>d__.end = <>3__end;
        return <GetDiff>d__;
    }

    [DebuggerHidden]
    IEnumerator IEnumerable.GetEnumerator()
    {
        return System.Collections.Generic.IEnumerable<System.Int32>.GetEnumerator();
    }
}

Давайте начнем с конструктора:

public <GetDiff>d__1(int <>1__state)
{
    this.<>1__state = <>1__state;
    <>l__initialThreadId = Environment.CurrentManagedThreadId;
}

Мы <>1__state в <>1__state значение, которое мы передали. Помните, что это был -2 (new <GetDiff>d__1(-2)). Мы также храним идентификатор вызывающего потока.

Первое, что будет делать клиентский цикл foreach, - это вызвать GetEnumerator в перенастроенном IEnumerable<int>. Там есть некоторая логика:

[DebuggerHidden]
IEnumerator<int> IEnumerable<int>.GetEnumerator()
{
     <GetDiff>d__1 <GetDiff>d__;
    if (<>1__state == -2 && <>l__initialThreadId == Environment.CurrentManagedThreadId)
    {
        <>1__state = 0;
        <GetDiff>d__ = this;
    }
    else
    {
        <GetDiff>d__ = new <GetDiff>d__1(0);
    }
    <GetDiff>d__.start = <>3__start;
    <GetDiff>d__.end = <>3__end;
    return <GetDiff>d__;
}

Он проверяет, что потребитель является тем же потоком и что это состояние не изменилось (по -2 с исходным -2), если оно сохраняется, оно возвращает себя. В противном случае возвращается клон. Это означает, что если GetEnumerator вызывается из другого потока или вызывается тем же потоком после начала итерации, возвращаемый IEnumerator<int> запускается с начала (как и должно быть).

Также обратите внимание, что GetEnumerator меняет состояние на 0. Это важно.

Теперь обратите внимание на метод MoveNext. Это конечный автомат, эквивалентный вашему коду:

private bool MoveNext()
{
    switch (<>1__state)
    {
    default:
        return false;
    case 0:
        <>1__state = -1;
        break;
    case 1:
        <>1__state = -1;
        start++;
        break;
    }
    if (start < end)
    {
        <>2__current = start;
        <>1__state = 1;
        return true;
    }
    return false;
}

При первом вызове состояние равно 0, код входит в switch и устанавливает состояние в -1.

После switch код проверяет, если start < end. Это проверка, чтобы увидеть, если он войдет в ваш while цикл. Если он не входит, он просто возвращает false и все готово. Если он входит, вы yield return start, поэтому он помещает start в <>2__current, изменяет состояние на 1 и возвращает true. Поскольку он вернул true, клиент foreach считывает текущее значение, выполняет цикл и снова вызывает MoveNext...

Во второй раз он входит в коммутатор, так как он находится в состоянии 1, затем снова меняет состояние на -1 и выполняет start++ которая была вашей следующей строкой... теперь ваши циклы while, а это означает, что мы должны проверить start < end, и это то, что он делает после switch. Если это условие все еще true, оно поместит новое значение start в <>2__current, изменит состояние и вернет true.

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


Для справки следующий код эквивалентен циклу foreach (источник):

{
    E e = ((C)(x)).GetEnumerator();
    try {
        while (e.MoveNext()) {
            V v = (V)(T)e.Current;
            embedded_statement
        }
    }
    finally {
        ... // Dispose e
    }
}

Итак, что же yield break; делать? В этом случае ничего. yield break; используется для указания того, что конечный автомат должен завершиться (MoveNext возвращает false), однако конечный автомат все равно завершится, потому что это конец метода. Как следствие, вы найдете только yield break; полезно (и имеет смысл), когда это не в конце метода. Например, см. Ответ Станислава Молчановского.

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

  • 0
    спасибо, отличный ответ! поэтому компилятор возвращает MoveNext = false для вас, и нам не нужно явно yield break в конце метода
0

Здесь вы использовали yield return и yield break,

yield return: возвращает значение в IEnumerable и продолжает со следующего оператора.

yield break: yield break работает как ключевое слово break. разрыв выхода используется для выхода из цикла

public static IEnumerable<int> GetDiff(int start, int end)
{
    while (true)
    {
        if(start > end) //Exit condition 
             yield break;   // this will exit from while loop

        yield return start; // This will return value of start to IEnumerable 
            start++;
    }
    //yield break; // No need to use here
}

Теперь вы можете использовать функцию GetDiff() как,

public static void Print()
{
    foreach(var item in GetDiff(1, 10))
        Console.WriteLine(item);
}

POC: .Net Fiddle

Ещё вопросы

Сообщество Overcoder
Наверх
Меню