我可以在yield-return-method中使用“ using”吗?

时间:2019-10-18 11:06:46

标签: c# using yield-return

我刚刚看了一个YouTube视频,在该视频中,辅导老师使用了yield return方法打开文件并从中读取文件,然后将文件返回yield给调用者(实际代码在FileStream的using块中)。

然后我想知道,是否可以在yield-return方法中使用“ using”或“ try-finally”。因为我的理解是,该方法仅在从中获得值时才运行。例如,使用“ Any()”在第一次收益率返回(或收益率下降)之后完成该方法。

因此,如果函数永不结束,那么何时执行finally块?使用这样的结构是否安全?

2 个答案:

答案 0 :(得分:3)

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";
}

我不会粘贴整个生成的状态机,因为它非常大。 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块中的状态。

(如果我们写了yield return Foo()Foo()抛出了一个例外,则状态-3似乎是一个警卫:在这种情况下,我们将停留在状态-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节中找到相应的规范:

  
      
  • 如果枚举器对象的状态被挂起,则调用Dispose:      
        
    • 将状态更改为“运行”。
    •   
    • 执行任何finally块,就像最后执行的yield return语句是yield break语句一样。如果这导致引发异常并将其传播到迭代器主体之外,则将枚举器对象的状态设置为after,并将该异常传播给Dispose方法的调用者。
    •   
    • 将状态更改为之后。
    •   
  •   

答案 1 :(得分: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()之后,我已经在"Destuctor called"消息之前收到了"Call Take().ToArray()"消息。

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

我挖得更深一些。似乎创建的IEnumerator<int>本身就是IDisposable。完成后,所有Linq方法都可能称为Dispose方法。

仅,如果我手动使用枚举器,则必须在其上调用Dispose。我认为,这就是它起作用的原因。