dispose 是通过 yield return 调用到底层的吗?
Is dispose called down to the bottom via yield return?
为了简单起见,我给出了愚蠢的例子。
IEnumerable<T> Silly<T>(this IEnumerable<T> source)
{
foreach(var x in source) yield return x;
}
我知道这会被编译成一个状态机。但它也类似于
IEnumerable<T> Silly<T>(this IEnumerable<T> source)
{
using(var sillier = source.GetEnumerator())
{
while(sillier.MoveNext()) yield return sillier.Current;
}
}
现在考虑这种用法
list.Silly().Take(2).ToArray();
这里可以看到Silly
enumerable 可能没有被完全消耗,但是Take(2)
itself 会被完全消耗。
问题: 当在 Take
枚举器上调用 dispose 时,它是否也会在 Silly 枚举器上调用 dispose,更具体地说是在 sillier
枚举器上调用 dispose?
我的猜测是,由于 foreach
,编译器可以处理这个简单的用例,但是不那么简单的用例呢?
IEnumerable<T> Silly<T>(this IEnumerable<T> source)
{
using(var sillier = source.GetEnumerator())
{
// move next can be called on different stages.
}
}
这会成为问题吗?因为大多数枚举器不使用非托管资源,但如果有人这样做,可能会导致内存泄漏。
如果不调用 dispose,如何使一次性可枚举?
一个想法:每个yield return
后面可以有一个if(disposed) yield break;
。现在 dispose 愚蠢枚举器的方法只需要设置 disposed = true
并移动枚举器一次以释放所有需要的东西。
C# 编译器在将您的迭代器转换为实际代码时会为您处理 很多。例如,这里的 MoveNext
包含第二个示例的实现 1:
private bool MoveNext()
{
try
{
switch (this.<>1__state)
{
case 0:
this.<>1__state = -1;
this.<sillier>5__1 = this.source.GetEnumerator();
this.<>1__state = -3;
while (this.<sillier>5__1.MoveNext())
{
this.<>2__current = this.<sillier>5__1.Current;
this.<>1__state = 1;
return true;
Label_005A:
this.<>1__state = -3;
}
this.<>m__Finally1();
this.<sillier>5__1 = null;
return false;
case 1:
goto Label_005A;
}
return false;
}
fault
{
this.System.IDisposable.Dispose();
}
}
因此,您会注意到 using
中的 finally
子句根本不存在,它是一个状态机2依赖于处于某些良好 (>= 0) 状态才能取得进一步的进展。 (这也是非法的 C#,但是嘿嘿)。
现在让我们看看它的 Dispose
:
[DebuggerHidden]
void IDisposable.Dispose()
{
switch (this.<>1__state)
{
case -3:
case 1:
try
{
}
finally
{
this.<>m__Finally1();
}
break;
}
}
所以我们可以看到 <>m__Finally1
在这里被调用(以及由于在 MoveNext
.
中退出 while
循环
和<>m__Finally1
:
private void <>m__Finally1()
{
this.<>1__state = -1;
if (this.<sillier>5__1 != null)
{
this.<sillier>5__1.Dispose();
}
}
因此,我们可以看到 sillier
已被处置 并且 我们进入了否定状态,这意味着 MoveNext
不必执行 任何特殊工作来处理"we've already been disposed state".
所以,
An Idea: there can be a if(disposed) yield break; after every yield return. now dispose method of silly enumerator will just have to set disposed = true and move the enumerator once to dispose all the required stuff.
完全没有必要。相信编译器会转换代码,以便它完成它应该做的所有 逻辑 事情 - 它只运行它的 finally 子句一次,当它用尽迭代器逻辑或当它被显式处理时。
1.NET Reflector 生成的所有代码示例。但是现在它太擅长反编译这些构造了,所以如果你去看看 Silly
方法本身:
[IteratorStateMachine(typeof(<Silly>d__1)), Extension]
private static IEnumerable<T> Silly<T>(this IEnumerable<T> source)
{
IEnumerator<T> <sillier>5__1;
using (<sillier>5__1 = source.GetEnumerator())
{
while (<sillier>5__1.MoveNext())
{
yield return <sillier>5__1.Current;
}
}
<sillier>5__1 = null;
}
它再次设法隐藏了有关该状态机的大部分细节。您需要追踪 IteratorStateMachine
属性引用的类型才能看到上面显示的所有细节。
2另请注意,编译器没有义务生成状态机以允许迭代器工作。它是当前 C# 编译器的实现细节。 C# 规范对编译器如何 转换迭代器没有任何限制,只是限制效果应该是什么。
为了简单起见,我给出了愚蠢的例子。
IEnumerable<T> Silly<T>(this IEnumerable<T> source)
{
foreach(var x in source) yield return x;
}
我知道这会被编译成一个状态机。但它也类似于
IEnumerable<T> Silly<T>(this IEnumerable<T> source)
{
using(var sillier = source.GetEnumerator())
{
while(sillier.MoveNext()) yield return sillier.Current;
}
}
现在考虑这种用法
list.Silly().Take(2).ToArray();
这里可以看到Silly
enumerable 可能没有被完全消耗,但是Take(2)
itself 会被完全消耗。
问题: 当在 Take
枚举器上调用 dispose 时,它是否也会在 Silly 枚举器上调用 dispose,更具体地说是在 sillier
枚举器上调用 dispose?
我的猜测是,由于 foreach
,编译器可以处理这个简单的用例,但是不那么简单的用例呢?
IEnumerable<T> Silly<T>(this IEnumerable<T> source)
{
using(var sillier = source.GetEnumerator())
{
// move next can be called on different stages.
}
}
这会成为问题吗?因为大多数枚举器不使用非托管资源,但如果有人这样做,可能会导致内存泄漏。
如果不调用 dispose,如何使一次性可枚举?
一个想法:每个yield return
后面可以有一个if(disposed) yield break;
。现在 dispose 愚蠢枚举器的方法只需要设置 disposed = true
并移动枚举器一次以释放所有需要的东西。
C# 编译器在将您的迭代器转换为实际代码时会为您处理 很多。例如,这里的 MoveNext
包含第二个示例的实现 1:
private bool MoveNext()
{
try
{
switch (this.<>1__state)
{
case 0:
this.<>1__state = -1;
this.<sillier>5__1 = this.source.GetEnumerator();
this.<>1__state = -3;
while (this.<sillier>5__1.MoveNext())
{
this.<>2__current = this.<sillier>5__1.Current;
this.<>1__state = 1;
return true;
Label_005A:
this.<>1__state = -3;
}
this.<>m__Finally1();
this.<sillier>5__1 = null;
return false;
case 1:
goto Label_005A;
}
return false;
}
fault
{
this.System.IDisposable.Dispose();
}
}
因此,您会注意到 using
中的 finally
子句根本不存在,它是一个状态机2依赖于处于某些良好 (>= 0) 状态才能取得进一步的进展。 (这也是非法的 C#,但是嘿嘿)。
现在让我们看看它的 Dispose
:
[DebuggerHidden]
void IDisposable.Dispose()
{
switch (this.<>1__state)
{
case -3:
case 1:
try
{
}
finally
{
this.<>m__Finally1();
}
break;
}
}
所以我们可以看到 <>m__Finally1
在这里被调用(以及由于在 MoveNext
.
while
循环
和<>m__Finally1
:
private void <>m__Finally1()
{
this.<>1__state = -1;
if (this.<sillier>5__1 != null)
{
this.<sillier>5__1.Dispose();
}
}
因此,我们可以看到 sillier
已被处置 并且 我们进入了否定状态,这意味着 MoveNext
不必执行 任何特殊工作来处理"we've already been disposed state".
所以,
An Idea: there can be a if(disposed) yield break; after every yield return. now dispose method of silly enumerator will just have to set disposed = true and move the enumerator once to dispose all the required stuff.
完全没有必要。相信编译器会转换代码,以便它完成它应该做的所有 逻辑 事情 - 它只运行它的 finally 子句一次,当它用尽迭代器逻辑或当它被显式处理时。
1.NET Reflector 生成的所有代码示例。但是现在它太擅长反编译这些构造了,所以如果你去看看 Silly
方法本身:
[IteratorStateMachine(typeof(<Silly>d__1)), Extension]
private static IEnumerable<T> Silly<T>(this IEnumerable<T> source)
{
IEnumerator<T> <sillier>5__1;
using (<sillier>5__1 = source.GetEnumerator())
{
while (<sillier>5__1.MoveNext())
{
yield return <sillier>5__1.Current;
}
}
<sillier>5__1 = null;
}
它再次设法隐藏了有关该状态机的大部分细节。您需要追踪 IteratorStateMachine
属性引用的类型才能看到上面显示的所有细节。
2另请注意,编译器没有义务生成状态机以允许迭代器工作。它是当前 C# 编译器的实现细节。 C# 规范对编译器如何 转换迭代器没有任何限制,只是限制效果应该是什么。