使用using释放嵌套对象

6
如果我有嵌套对象的代码,例如下面所示,我是否需要使用嵌套的using语句来确保SQLCommand和SQLConnection对象都被正确处理,或者如果实例化SQLCommand的代码在外部using语句中,那么我是否可以?
using (var conn = new SqlConnection(sqlConnString))
{
    using (var cmd = new SqlCommand())
    {
        cmd.CommandType = CommandType.Text;
        cmd.CommandText = cmdTextHere;
        conn.Open();

        cmd.Connection = conn;
        rowsAffected = cmd.ExecuteNonQuery();
    }
}
4个回答

9
是的。你可以像这样稍微整理一下:

using (SqlConnection conn = new SqlConnection(sqlConnString))
using (System.Data.SqlClient.SqlCommand cmd = new System.Data.SqlClient.SqlCommand())
{
   // code here
}

但你仍需要为每个IDisposable对象使用using

编辑:考虑以下未使用内部using语句的示例。

class A : IDisposable
{
    public void Dispose()
    {
        Console.WriteLine("A Disposed");
    }
}

class B : IDisposable
{
    public void Dispose()
    {
        Console.WriteLine("B Disposed");
    }
}

代码

using (A a = new A())            
{
    B b = new B();
}

在代码块结束时,A被正确处理。那么B会发生什么呢?它超出了作用域,只是等待被垃圾回收。B.Dispose()没有被调用。
using (A a = new A())
using (B b = new B())
{
}

当执行离开块(实际上是 )时,编译的代码会执行对每个对象的Dispose()方法调用。


为什么总是要显式地指定呢?这个回答偏离了问题的真正目的。 - Nayan
@Nayan,请看我的评论回答。 - Anthony Pegram
注意:不要对Stream类执行此操作,因为它们拥有传递的流!请参见https://dev59.com/THNA5IYBdhLWcg3wKam3。 - fmuecke

5

您可以省略 SqlCommand 上的 using 语句。GC 最终会为您清理它。但是,我强烈建议您不要这样做。我将解释为什么。

SqlCommand 间接继承自 System.ComponentModel.Component,因此继承了它的 Finalizer 方法。不调用 SqlCommand 的 dispose 方法将确保该命令在超出范围后至少提升一代(.NET 垃圾收集器是分代 GC)。例如:当命令在第 1 代时,它将移动到第 2 代。可终结对象会在内存中保留更长时间,以确保终结器可以安全地运行。但不仅命令本身会在内存中保留,而且所有引用它的对象也会随之进入该代。它将引用的对象包括 SqlConnectionSqlParameter 对象列表、可能很大的 CommandText 字符串和许多其他内部对象。只有在收集该代时,才能删除该内存,但是代数越高,清理频率越低。

不调用将导致额外的内存压力和终结器线程的额外工作。当.NET无法分配新内存时,CLR将强制对所有代进行垃圾回收。之后,运行时通常会再次有足够的空间来分配新对象。然而,当这种强制回收发生在内存中仍有许多对象需要提升到下一代(因为它们是可终结的或被可终结对象引用)时,可能CLR无法释放足够的内存。这将导致“OutOfMemoryException”的结果。我必须承认我从未见过这种情况发生,因为开发人员没有正确处理他们的SqlCommand对象的释放。然而,在生产系统中,我经常看到由于不正确地释放对象而导致的OOMs。我希望这能为GC如何工作以及不正确处理(可终结)对象的风险提供一些背景。我总是处理所有可处置的对象。虽然查看反编译器可以证明这并不一定适用于某个类型,但这种编程方式会导致代码难以维护,并使代码依赖于类型的内部行为(这种行为可能会在未来发生变化)。

4

您应该对两者都调用 dispose,不过,如果您只是这样做,那么代码会更易于阅读:

using (SqlConnection conn = new SqlConnection(sqlConnString)) 
using (SqlCommand cmd = new SqlCommand()) 
{ 
     cmd.CommandType = CommandType.Text; 
     cmd.CommandText = cmdTextHere; 
     conn.Open(); 

     cmd.Connection = conn; 
     rowsAffected = cmd.ExecuteNonQuery(); 
} 

由于有一个隐式块被创建(就像if语句或for语句一样),并且使用是一个完整的块,因此您不需要大括号,并且在我看来更加整洁。有些人会说您应该始终使用大括号,因为很容易意外添加第二个语句并创建错误,但在这种情况下,我认为这是不太可能的。


整洁,是的。但它仍然没有回答问题。:/ - Nayan
1
是的,确实如此。你应该在两者上都调用dispose方法,因此在两个using语句中都要这样做。 - Erik Funkenbusch

0
回答你的问题,是的,你可以这样做。
由于SqlCommand对象受外部大括号的限制,当执行超出该块时,它将被GC收集。
其他答案也可以,但它们并没有确切地回答你的问题 :)

3
垃圾回收不同于调用.Dispose()方法。此外,GC是不确定的,你无法精确知道对象何时被收集。 - Anthony Pegram
依赖GC来收集SqlCommand实例可能会导致非常严重的内存“泄漏”,使许多实例留在内存中。 - Marek
我同意Marek的观点。请阅读我的回答。这是Marek回答的长版本 :-). - Steven

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