我可以在 yield-return 方法中使用 "using" 吗?

Can I use "using" in a yield-return-method?

我刚刚看到一个 YouTube 视频,其中导师使用 yield return 方法打开文件并从中读取行,这些行被 yield-returned 给调用者(实际代码位于 FileStream 周围的 using 块中。

然后我想知道,在yield-return方法中使用"using"或"try-finally"是否可以。因为我的理解是,该方法只会运行很长时间,因为值是从中获取的。以 "Any()" 为例,该方法在第一个 yield return(当然是 yield break)之后完成。

那么,如果函数永远不会运行到结束,那么finally块什么时候执行呢?使用这样的结构安全吗?

刚刚写了一些测试代码,好像每次都在适当的时候调用了析构函数。

struct Test : IDisposable
{
    public void Dispose() => Console.WriteLine("Destructor called");
}

static IEnumerable<int> InfiniteInts()
{
    Console.WriteLine("Constructor Called");
    using(var test = new Test()) {
        int i = 0;
        while(true)
            yield return ++i;
    }
}

static void Main(string[] args)
{
    var seq = InfiniteInts();
    Console.WriteLine("Call Any()");
    bool b = seq.Any();
    Console.WriteLine("Call Take().ToArray()");
    int[] someInts = seq.Take(20).ToArray();

    Console.WriteLine("foreach loop");
    foreach(int i in seq)
    {
        if(i > 20) break;
    }

    Console.WriteLine("do it manually: while loop");
    var enumerator = seq.GetEnumerator();
    while(enumerator.MoveNext())
    {
         int i = enumerator.Current;
         if(i > 20) break;
    }
    Console.WriteLine("No destructor call has happened!");

    enumerator.Dispose();
    Console.WriteLine("Now destructor has beend called");

    Console.WriteLine("End of Block");
}

在我调用seq.Any()之后,我已经在"Call Take().ToArray()"消息之前收到了"Destuctor called"消息。

seq.Take(20).ToArray() 语句也是如此。调用了析构函数。

我挖得更深了一点。看起来创建的 IEnumerator<int> 本身就是一个 IDisposable。所有 Linq 方法都可能在完成后调用此 Dispose 方法。

只是,如果我手动使用枚举器,我必须对其调用 Dispose。我想,这就是它起作用的原因。

IEnumerator<T> 实现 IDisposable,而 foreach 循环将在完成时处理它们正在枚举的东西(这包括使用 [=16= 的 linq 方法) ]循环,如.ToArray()).

事实证明,编译器为生成器方法生成的状态机以一种聪明的方式实现了 Dispose:如果状态机处于 "inside" 和 using 的状态块,然后在状态机上调用 Dispose() 将处理受 using 语句保护的事物。


举个例子:

public IEnumerable<string> M() {
    yield return "1";
    using (var ms = new MemoryStream())
    {
        yield return "2";  
        yield return "3";
    }
    yield return "4";
}

我不打算粘贴整个生成的状态机,因为它非常大。 You can see it on SharpLab here.

状态机的核心是以下 switch 语句,它跟踪我们通过每个 yield return 语句的进度:

switch (<>1__state)
{
    default:
        return false;
    case 0:
        <>1__state = -1;
        <>2__current = "1";
        <>1__state = 1;
        return true;
    case 1:
        <>1__state = -1;
        <ms>5__1 = new MemoryStream();
        <>1__state = -3;
        <>2__current = "2";
        <>1__state = 2;
        return true;
    case 2:
        <>1__state = -3;
        <>2__current = "3";
        <>1__state = 3;
        return true;
    case 3:
        <>1__state = -3;
        <>m__Finally1();
        <ms>5__1 = null;
        <>2__current = "4";
        <>1__state = 4;
        return true;
    case 4:
        <>1__state = -1;
        return false;
}

您可以看到我们在进入状态 2 时创建了 MemoryStream,并在我们退出状态 3 时处理它(通过调用 <>m__Finally1())。

这是 Dispose 方法:

void IDisposable.Dispose()
{
    int num = <>1__state;
    if (num == -3 || (uint)(num - 2) <= 1u)
    {
        try
        {
        }
        finally
        {
            <>m__Finally1();
        }
    }
}

如果我们处于状态 -3、2 或 3,那么我们将调用 <>m__Finally1();。状态 2 和 3 是 using 块内的状态。

(状态 -3 似乎是一个守卫,以防我们写 yield return Foo()Foo() 抛出异常:在这种情况下,我们将停留在状态 -3 并且无法迭代任何更进一步。但是,在这种情况下,我们仍然可以处理 MemoryStream)。

为了完整起见,<>m__Finally1 定义为:

private void <>m__Finally1()
{
    <>1__state = -1;
    if (<ms>5__1 != null)
    {
        ((IDisposable)<ms>5__1).Dispose();
    }
}

您可以在 C# Language Specification 部分 10.14.4.3:

中找到相关规范
  • If the state of the enumerator object is suspended, invoking Dispose:
    • Changes the state to running.
    • Executes any finally blocks as if the last executed yield return statement were a yield break statement. If this causes an exception to be thrown and propagated out of the iterator body, the state of the enumerator object is set to after and the exception is propagated to the caller of the Dispose method.
    • Changes the state to after.