使用 "using" 和 "finally" 清理资源

5
这种结构何时需要使用?
using (Something something = new Something())
{
    try
    {
    }
    finally
    {
        something.SomeCleanup();
    }
}

或者说,是否应该在隐式的something.Dispose()调用中执行所有清理任务?以下是有问题的代码:
public static DataTable GetDataTable(string cmdText, IEnumerable<Parameter> parameters)
{
    // Create an empty memory table.
    DataTable dataTable = new DataTable();

    // Open a connection to the database.
    using (SqlConnection connection = new SqlConnection(ConfigurationTool.ConnectionString))
    {
        connection.Open();

        // Specify the stored procedure call and its parameters.
        using (SqlCommand command = new SqlCommand(cmdText, connection))
        {
            command.CommandType = CommandType.StoredProcedure;

            SqlParameterCollection parameterCollection = command.Parameters;
            foreach (Parameter parameter in parameters)
                parameterCollection.Add(parameter.SqlParameter);

            try
            {
                // Execute the stored procedure and retrieve the results in the table.
                using (SqlDataAdapter dataAdapter = new SqlDataAdapter(command))
                    try
                    {
                        dataAdapter.Fill(dataTable);
                    }
                    catch
                    {
                        dataTable.Dispose();
                        dataTable = null;
                    }
            }
            finally
            {
                //parameterCollection.Clear();
            }
        }
    }

    return dataTable;
}

注意:我已经定义了Parameter类,以便这个函数的用户不必直接处理SqlParameter的创建。可以使用Parameter类的SqlParameter属性来检索SqlParameter

在某个时刻,我的程序执行以下操作(无法发布代码,因为涉及到很多类;基本上,我有一个小型框架创建了许多对象):

  1. 创建一个Parameter数组。
  2. GetDataTable('sp_one', parameters)
  3. GetDataTable('sp_two', parameters)

你创建了Something类吗?如果是,为什么不把所有的清理操作都放在Dispose方法中呢? - mbeckish
@mbeckish: 我没有创建 Something 类。实际上,我的 Something 类是微软自己的 SqlConnectionSqlParameter。常识告诉我们微软的程序员应该足够聪明,在 Dispose 方法中关闭打开的数据库连接(当然,并不意味着他们不应该提供 Close 方法),但文档中并没有说明。我真的很想使用 C++ 并访问 STL 的源代码。 - isekaijin
我非常确定 SqlConnection.DisposeClose 做的事情是一样的(除了能够在 Close 后重新打开而不是 Dispose)。如果您的连接实际上没有关闭,可能是由于连接池。顺便说一下,您实际上可以访问源代码,它已经发布了,还可以使用 Reflector http://reflector.red-gate.com/download.aspx?TreatAsUpdate=1 - František Žiačik
@František Žiačik:我不知道SqlConnection.Dispose实际上关闭了连接,所以这是我的无知的错。但是我非常确定,当我尝试将SqlParameter第二次添加到SqlParameterCollection时,即使第一个SqlParameterCollection属于已经释放的SqlCommand,我仍然会收到“Another SqlParameterCollection contains the SqlParameter”错误。 - isekaijin
为什么不直接创建新的SqlParameter?这只是一个设计决策问题,有人决定一个SqlParameter只能添加到一个SqlParameterCollection中(我不知道为什么),与Dispose模式无关。您不需要担心内存,垃圾回收器会帮助您处理。 - František Žiačik
3个回答

5
using关键字仅调用.Dispose()方法。如果您在IDisposable对象上有必要的清理工作发生在dispose方法之外,那么您需要在自己的finally块中完成这项工作。这带来了两个问题:
  1. 此时,您可以认为您可能会跳过使用块,只需在finally块中调用Dispose()。就我个人而言,我仍然会使用using块。始终为IDisposable实例使用using块是一个好习惯。
  2. 我谦虚地建议,如果您满足上述条件,则需要重新设计类以利用IDisposable模式。

基于您发布的代码,问题在于您的参数仍然根源于某个地方(也许您正在重复使用它们?)。因为参数仍然根源于它们无法被收集。它们还包含对它们所附加到的命令的引用,因此您的SqlCommand对象也不能立即被收集,因为现在它仍然是根源。
关键点是.Net框架保留Dispose()模式用于非托管资源。因为SqlParameters和SqlParameterCollection是托管类型,所以它们不会被触及,直到它们被收集,这完全与处置分开。当最终收集您的SqlCommand时,它的SqlParameter集合也将得到处理。只是不要混淆收集、处置和其目的。
您想做的是在添加参数时制作每个参数的副本,而不是将现有参数添加到集合中。
public static DataTable GetDataTable(string cmdText, IEnumerable<Parameter> parameters)
{
    // Create an empty memory table.
    DataTable dataTable = new DataTable();

    // Prepare a connection to the database and command to execute.
    using (SqlConnection connection = new SqlConnection(ConfigurationTool.ConnectionString))
    using (SqlCommand command = new SqlCommand(cmdText, connection))
    {
        command.CommandType = CommandType.StoredProcedure;

        SqlParameterCollection parameterCollection = command.Parameters;
        foreach (Parameter parameter in parameters)
            parameterCollection.Add(CloneParameter(parameter.SqlParameter));

        // Execute the stored procedure and retrieve the results in the table.
        using (SqlDataAdapter dataAdapter = new SqlDataAdapter(command))
        {
             dataAdapter.Fill(dataTable);
        }
    }

    return dataTable;
}

需要注意的是:我成功地摆脱了你所有的try块,一个都不需要。此外,SqlDataAdapter.Fill()方法将为您打开和关闭连接,因此您不需要那部分内容。

现在让我们谈谈CloneParameter()函数。我有印象你认为它破坏了你代码的目的,即尝试重用参数。我向你保证,在这里重用参数是一个坏主意。与存储过程执行相比,性能损失可以忽略不计,几乎不存在。我把CloneParameter()的实现留给你,原因有两个:首先,它很简单;其次,我们已经超出了我的正常数据访问模式。我通常使用Action<SqlParameterCollection>委托来接受参数,而不是参数可枚举。函数的声明更像这样:

public IEnumerable<IDataRecord>GetData(string cmdText, Action<SqlParameterCollection> addParameters)

并且这样被称为:

foreach(var record in GetData("myprocedurename", p => 
  {
      p.Add( /*new parameter here*/ );
      p.Add( /*new parameter here*/ );
    //...
  })
 .Select( /*Returning a IEnumerable rather than a datatable allows me to use it with linq to objects.*/
          /* For example, you could use this spot to convert from DataRecords returned by ADO.Net to business objects */ 
        ))
{
   // use the results here...
}

由于您需要连续填写两个表格,这说明您在客户端可能需要进行一些工作来证明这比DataReader/IEnumerable方法更合适,但我想提醒一下,大多数情况下基于DataReader的代码是更好的选择。

如果按照我的Action委托模式并希望尽可能重复使用一组参数,则会使用一个真实的、命名的方法,该方法知道如何添加参数并匹配我的Action委托。然后,我只需传递该方法即可,从而获得所需的参数重用。


@Joel:实际上,“Something”就是微软自己的“SqlCommand”,很明显在被处理后它不会清除其参数集合。 - isekaijin
@Eduardo - 参数是由垃圾回收器清理的_managed_资源。不需要在参数集合上调用.Clear()。你可能唯一会遇到问题的时候是,如果你做了一些淘气的事情,使得参数集合仍然被引用,或者如果你在参数中存储了非常大的值,这些值最终会进入大对象堆,即使在这种情况下,清除参数集合也没有帮助。 - Joel Coehoorn
@Eduardo - 我将给你的是SqlDataReader,它的Dispose()方法并不会关闭它,但即使如此,只有在你依赖于CommandBehavior.CloseConnection时才会出现问题。 - Joel Coehoorn
@Eduardo - 好的,已更新。重要的部分是断点后的第一段。那里解释了正在发生的事情。 - Joel Coehoorn
@Joel:你是怎么得出 SqlDataReader 在被释放时没有被关闭的结论的?Dispose 方法肯定会调用 Close。还是说你想表达的是 Close 方法本身实际上并没有关闭读取器? - LukeH
@LukeH - 你说得对。以前确实是这样出现问题的,但是最新版本的框架似乎已经修复了这个问题。 - Joel Coehoorn

4

有趣的问题!

这完全取决于你的Something类。如果它设计得不好,需要进行多阶段清理,那么它会强制将其特殊性传递给客户端。

你不应该设计成这样的类。如果你需要进行中间清理,请将其封装在自己的类中,并使用以下代码:

using (Something something = new Something()) {
  // ...
  using (SomethingElse somethingElse = something.GiveMeSomethingElse()) {
  }
  // ...
} 

更新:

以您的例子为例,可能会像这样:

using (SqlConnection connection = new SqlConnection(connectionString)) {
  connection.Open();

  using (SqlCommand command = new SqlCommand("select * from MyTable where id = @id", connection)) {

    // to "reuse" the parameters collection population, just extract this to a separate method      
    command.Parameters.Add(new SqlParameter("id", id));

    // ... execute the command

  }

}

更新2:

只需要这样做:

GetDataTable('sp_one', CreateParameters());
GetDataTable('sp_two', CreateParameters());

我有一个函数,它的参数是存储过程的名称和一个SqlParameter数组。这些参数可以在多个存储过程调用中重复使用。不幸的是,SqlCommand在被处理时显然不清除自己的参数集合。 - isekaijin
那么,您可以为每次调用该方法重新创建参数。您重复使用的是代码,而不是运行时参数数组。 - Jordão
虽然典型的 SqlParameter 对象不是特别沉重的对象,但如果没有必要,为什么还要创造更多的对象呢? - isekaijin
这是一个真正的瓶颈吗?我认为,如果涉及到数据库调用,你就不需要考虑优化应用程序的这个方面。 - Jordão
我认为这里的问题是你还在用C++的思维方式,而应该转换成C#的思维方式。这意味着让垃圾收集器来处理它。 - František Žiačik

1

Dispose应该清理所有未托管的资源。拥有另一个清理方法,例如执行一些功能或数据库操作,是完全可行的。


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