我刚刚看了一个YouTube视频,在该视频中,辅导老师使用了yield return方法打开文件并从中读取文件,然后将文件返回yield给调用者(实际代码在FileStream的using块中)。
然后我想知道,是否可以在yield-return方法中使用“ using”或“ try-finally”。因为我的理解是,该方法仅在从中获得值时才运行。例如,使用“ Any()”在第一次收益率返回(或收益率下降)之后完成该方法。
因此,如果函数永不结束,那么何时执行finally块?使用这样的结构是否安全?
答案 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。我认为,这就是它起作用的原因。