在通用 IEnumerable 上使用 yield return 时提前退出调用代码
Early exit from calling code when using yield return on a generic IEnumerable
当调用代码在完成对正在返回的 IEnumerable 的枚举之前退出时会发生什么。
一个简化的例子:
public void HandleData()
{
int count = 0;
foreach (var datum in GetFileData())
{
//handle datum
if (++count > 10)
{
break;//early exit
}
}
}
public static IEnumerable<string> GetFileData()
{
using (StreamReader sr = _file.BuildStreamer())
{
string line = String.Empty;
while ((line = sr.ReadLine()) != null)
{
yield return line;
}
}
}
在这种情况下,及时关闭 StreamReader 似乎非常重要。是否需要一种模式来处理这种情况?
这是个好问题。
你看,在使用 foreach() 迭代生成的 IEnumerable 时,你是安全的。下面的 Enumerator 实现了 IDisposable 本身,它在 foreach 的情况下被调用(即使使用 break 退出循环)并负责清理 GetFileData 中的状态。
但是如果你直接玩Enumerator.MoveNext,你就有麻烦了,如果提前退出,Dispose 将永远不会被调用(当然,如果你将完成手动迭代,它将会)。对于手动基于枚举器的迭代,您也可以考虑将枚举器放在 using 语句中(如下面的代码所述)。
希望这个包含不同用例的示例能为您的问题提供一些反馈。
static void Main(string[] args)
{
// Dispose will be called
foreach(var value in GetEnumerable())
{
Console.WriteLine(value);
break;
}
try
{
// Dispose will be called even here
foreach (var value in GetEnumerable())
{
Console.WriteLine(value);
throw new Exception();
}
}
catch // Lame
{
}
// Dispose will not be called
var enumerator = GetEnumerable().GetEnumerator();
// But if enumerator and this logic is placed inside the "using" block,
// like this: using(var enumerator = GetEnumerable().GetEnumerable(){}), it will be.
while(enumerator.MoveNext())
{
Console.WriteLine(enumerator.Current);
break;
}
Console.WriteLine("{0}Here we'll see dispose on completion of manual enumeration.{0}", Environment.NewLine);
// Dispose will be called: ended enumeration
var enumerator2 = GetEnumerable().GetEnumerator();
while (enumerator2.MoveNext())
{
Console.WriteLine(enumerator2.Current);
}
}
static IEnumerable<string> GetEnumerable()
{
using (new MyDisposer())
{
yield return "First";
yield return "Second";
}
Console.WriteLine("Done with execution");
}
public class MyDisposer : IDisposable
{
public void Dispose()
{
Console.WriteLine("Disposed");
}
}
最初观察者:https://blogs.msdn.microsoft.com/dancre/2008/03/15/yield-and-usings-your-dispose-may-not-be-called/
作者称之为(手动 MoveNext() 和早期中断不会触发 Dipose() 的事实)"a bug",但这是预期的实现。
当调用代码在完成对正在返回的 IEnumerable 的枚举之前退出时会发生什么。
一个简化的例子:
public void HandleData()
{
int count = 0;
foreach (var datum in GetFileData())
{
//handle datum
if (++count > 10)
{
break;//early exit
}
}
}
public static IEnumerable<string> GetFileData()
{
using (StreamReader sr = _file.BuildStreamer())
{
string line = String.Empty;
while ((line = sr.ReadLine()) != null)
{
yield return line;
}
}
}
在这种情况下,及时关闭 StreamReader 似乎非常重要。是否需要一种模式来处理这种情况?
这是个好问题。
你看,在使用 foreach() 迭代生成的 IEnumerable 时,你是安全的。下面的 Enumerator 实现了 IDisposable 本身,它在 foreach 的情况下被调用(即使使用 break 退出循环)并负责清理 GetFileData 中的状态。
但是如果你直接玩Enumerator.MoveNext,你就有麻烦了,如果提前退出,Dispose 将永远不会被调用(当然,如果你将完成手动迭代,它将会)。对于手动基于枚举器的迭代,您也可以考虑将枚举器放在 using 语句中(如下面的代码所述)。
希望这个包含不同用例的示例能为您的问题提供一些反馈。
static void Main(string[] args)
{
// Dispose will be called
foreach(var value in GetEnumerable())
{
Console.WriteLine(value);
break;
}
try
{
// Dispose will be called even here
foreach (var value in GetEnumerable())
{
Console.WriteLine(value);
throw new Exception();
}
}
catch // Lame
{
}
// Dispose will not be called
var enumerator = GetEnumerable().GetEnumerator();
// But if enumerator and this logic is placed inside the "using" block,
// like this: using(var enumerator = GetEnumerable().GetEnumerable(){}), it will be.
while(enumerator.MoveNext())
{
Console.WriteLine(enumerator.Current);
break;
}
Console.WriteLine("{0}Here we'll see dispose on completion of manual enumeration.{0}", Environment.NewLine);
// Dispose will be called: ended enumeration
var enumerator2 = GetEnumerable().GetEnumerator();
while (enumerator2.MoveNext())
{
Console.WriteLine(enumerator2.Current);
}
}
static IEnumerable<string> GetEnumerable()
{
using (new MyDisposer())
{
yield return "First";
yield return "Second";
}
Console.WriteLine("Done with execution");
}
public class MyDisposer : IDisposable
{
public void Dispose()
{
Console.WriteLine("Disposed");
}
}
最初观察者:https://blogs.msdn.microsoft.com/dancre/2008/03/15/yield-and-usings-your-dispose-may-not-be-called/
作者称之为(手动 MoveNext() 和早期中断不会触发 Dipose() 的事实)"a bug",但这是预期的实现。