不使用using、foreach或手动调用Dispose()时的枚举器处理方式

10

我正在使用yield return来遍历一个SqlDataReader的记录:

IEnumerable<Reading> GetReadings() {
    using (var connection = new SqlConnection(_connectionString))
    {
        using (var command = new SqlCommand(_query, connection))
        {
            connection.Open();
            using (var reader = command.ExecuteReader())
            {
                while (reader.Read())
                {
                    yield return new Reading
                    {
                        End = reader.GetDateTime(0),
                        Value = reader.GetDouble(1)
                    };
                }
            }
            connection.Close();
        }
    }
}

我随后使用了一个改编版的这个被接受的答案来将许多迭代器一起“压缩”:

   var enumerators = data.Select(d => new
   {
       d.Key,
       Enumerator = d.Value.GetEnumerator()
   }).ToList();

   while (true)
   {
       foreach (var item in enumerators)
       {
           if (!item.Enumerator.MoveNext())
           {
               yield break;
           }

           /*snip*/
       }

      /*snip*/
   }
在上述方法中,枚举器的Dispose()没有被显式调用,并且它们没有在usingforeach语句中使用,那么基础迭代器会保持打开状态吗?在我这个例子中,使用了一个打开的SqlConnection

我是否应该调用枚举器的Dispose()来确保整个下游链路都关闭了呢?


3
为什么不使用 using 语法?当块上下文结束时,包括 yieldreturn 时,它会自动调用。 - Yuck
在zipmany风格的方法中具有可变数量的枚举器使得每个枚举器都用using包装起来变得困难。 - Oliver
3个回答

28
当枚举这个迭代器时,如果枚举器的 Dispose() 没有被显式调用并且没有在 using 语句中使用,那么底层迭代器会保持打开状态吗?
让我将这个问题改写成更容易回答的形式。
当使用 foreach 枚举包含 using 语句的迭代器块时,控制离开循环时,资源是否被处理?
是的。
是什么机制确保了这一点?
以下是三个机制:
- using 语句只是编写 try-finally 的便捷方式,其中 finally 处理资源的处理。 - foreach 循环也是编写 try-finally 的便捷语法,并且再次在控件离开循环时调用枚举器上的 finally 调用 Dispose。 - 由迭代器块生成的枚举器实现 IDisposable。对其调用 Dispose() 可确保执行迭代器块中的所有 finally 块,包括来自 using 语句的 finally 块。
如果我避免使用 foreach 循环,自己调用 GetEnumerator 并且不在枚举器上调用 Dispose,那么我是否保证枚举器的 finally 块会运行?
没有。始终处理您的枚举器。它们实现了 IDisposable 有其原因。
现在清楚了吗?
如果您对此感兴趣,则应阅读我的关于 C# 中迭代器块设计特性的长系列文章。

http://blogs.msdn.com/b/ericlippert/archive/tags/iterators/

这是一个与迭代器相关的链接,链接指向Eric Lippert的博客。

item 本身就是 Enumerator,怎么办? - Hamlet Hakobyan
@HamletHakobyan:IEnumerator<T> 实现了 IDisposable 接口。如果你有一个 IDisposable 对象,但从未调用 Dispose() 方法,则该对象的清理代码将不会执行。 - Eric Lippert
1
是的,显然如此。我的评论取决于OP的问题,我看到问题和答案之间存在误导。 - Hamlet Hakobyan

0

您可以调用Dispose来释放连接分配的资源,并调用Close仅关闭连接。在Close调用之后,如果运行时发现合适的连接池,则可能将连接推入连接池;而在Dispose的情况下,则会被安排进行销毁。

因此,一切都取决于您所说的“有效”状态的含义。

如果它包含在using指令中,这只是try/finally,您可以保证即使在迭代中发生任何异常,连接也将被关闭并且其资源将被销毁。在其他情况下,您必须自己处理所有内容。


-1

为此添加一些细微差别:

数组枚举器不是通用的,也不可处置

集合枚举器是通用的,并且是可处置的,但仅当T本身是引用类型而非值类型时才是可处置的

因此,如果使用GetEnumerator(),请确保使用

IEnumerator arrayEnumerator = arrayToEnumerate.GetEnumerator();
IEnumerator<T> collectionEnumerator = collectionToEnumerate.GetEnumerator();

然后在你的代码中

collectionEnumerator.Dispose(); // compiles if T is reference-type but NOT value-type

示例:

object[] theObjectArray = [...]
IEnumerator arrayEnumerator = theObjectArray.GetEnumerator(); // does not return 
IEnumerator<object> and is not disposable
arrayEnumerator.Dispose(); // won't compile

List<double> theListOfDoubles = [...]
IEnumerator<someObject> doubleEnumerator = doubleEnumerator.GetEnumerator();
doubleEnumerator.Dispose(); // won't compile

List<someObject> theListOfObjects = [...]
IEnumerator<someObject> objectEnumerator = theListOfObjects.GetEnumerator(); // disposable
objectEnumerator.Dispose(); // compiles & runs

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