循环后是否需要调用“ yield break”?

时间:2019-04-13 06:01:01

标签: c#

在我的代码中,我有一个像这样的方法

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;循环之后的第二种情况下如何完成

2 个答案:

答案 0 :(得分:7)

否,这不是必需的。它将起作用:

||

在这种情况下,仅通过退出该函数即可结束该函数的执行。

但是您可以这样写:

return

答案 1 :(得分:2)

将您的代码放入编译器中,进行构建,然后反向工程回C#,结果如下:

using System.Collections.Generic;

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

我将LINQPad 5和ILSpy用于LINQPad。

那里没有产量中断。好吧,好吧,那仍然有糖分……让我们翻译成C#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的实例,设置其startend属性并返回它。我们将回到传递给其构造函数的-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中存储我们传递的值。请记住,这是一个-2new <GetDiff>d__1(-2))。我们还将存储调用线程的ID。

客户端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起)(如果成立),它将返回自身。否则,它将返回一个克隆。这意味着,如果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循环(source):

{
    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;不在方法末尾时才发现它有用(且有意义)。例如,请参见Stanislav Molchanovsky's answer

此外,我还认为添加yield break;不会有助于代码的可维护性或可读性。