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

3
我刚刚看了一个YouTube视频,在视频中,导师使用yield return方法打开文件并从中读取行,这些行将被yield返回给调用者(实际代码在FileStream周围的using块中)。
然后我想知道,在yield return方法中是否可以使用“using”或“try-finally”。因为我的理解是,只要从方法中获取值,该方法就会运行。例如使用“Any()”,在第一个yield return(或yield break)之后,该方法已经完成。
那么,如果该函数从未运行到结束,finally块何时执行?使用这种结构是否安全?
2个回答

4

IEnumerator<T> 实现了 IDisposable 接口,而 foreach 循环会在循环结束时释放它们正在枚举的内容(包括使用 foreach 循环的 linq 方法,例如 .ToArray())。

事实证明,生成器方法的编译器生成状态机以智能的方式实现了 Dispose:如果状态机处于“内部”using块所保护的状态,则调用状态机上的Dispose()将释放由using语句保护的对象。


让我们来看一个例子:

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

我不会粘贴整个生成的状态机,因为它非常大。您可以在这里查看
状态机的核心是以下 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# 语言规范的第10.14.4.3节中找到此规范:
  • 如果枚举器对象的状态已挂起,调用Dispose:
    • 将状态更改为运行中。
    • 执行任何finally块,就好像上一次执行的yield return语句是yield break语句。如果这导致异常被抛出并从迭代器体传播出去,则枚举器对象的状态被设置为after,并将异常传播给Dispose方法的调用方。
    • 将状态更改为after。

2
我刚刚写了一些测试代码,看起来析构函数在合适的时候总是被调用。
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方法。我认为这就是这个方法能够工作的原因。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接