【问题标题】:On using "using" and "finally" to cleanup resources关于使用“using”和“finally”清理资源
【发布时间】:2011-07-26 23:09:42
【问题描述】:

有没有需要如下结构的情况?

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 类,所以这个函数的用户不必直接处理SqlParameters 的创建。 Parameter 类的SqlParameter 属性可用于检索SqlParameter。

在某些时候,我的程序执行以下操作(无法发布代码,因为它涉及很多类;基本上,我有一个创建大量对象的迷你框架):

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

【问题讨论】:

  • 您创建了 Something 类吗?如果是这样,为什么不把所有的清理操作都放在 Dispose 方法中?
  • @mbeckish:我没有创建 Something 类。实际上,我的Something 类是微软自己的SqlConnection 和SqlParameter。常识说微软程序员足够聪明,可以在Dispose 方法中关闭打开的数据库连接(当然,这并不意味着他们也不应该提供Close 方法),但文档什么也没说。我真的很怀念使用 C++ 和访问 STL 的源代码。
  • 我很确定SqlConnection.Dispose 的作用与Close 的作用相同(除了能够在Close 之后重新打开,而不是Dispose)。如果您的连接实际上没有关闭,则可能是由于连接池。顺便说一句,你实际上可以访问源代码,它已经发布,你也可以使用反射器reflector.red-gate.com/download.aspx?TreatAsUpdate=1
  • @František Žiačik:我不知道SqlConnection.Dispose 实际上关闭了连接,所以这是我无知的错。但我很确定当我第二次尝试将 SqlParameter 添加到 SqlParameterCollection 时出现“另一个 SqlParameterCollection 包含 SqlParameter”错误,即使第一个 SqlParameterCollection 属于已经处置的 SqlCommand。
  • 你为什么不直接创建新的SqlParameter?这只是一个设计决策问题,其中有人决定只能将 SqlParameter 添加到单个 SqlParameterCollection(我不知道为什么)并且与 Dispose 模式无关。您无需担心内存问题,这就是垃圾收集器可以提供帮助的地方。

标签: .net using try-finally


【解决方案1】:

using 关键字仅调用.Dispose() 方法。如果您在 IDisposable 对象的 dispose 方法之外进行了 必要 清理,那么您将需要在它自己的 finally 块中执行此操作。这带来了两点:

  1. 此时,您可能会争辩说,您不妨跳过 using 块,也只在 finally 块中调用 Dispose()。就个人而言,我仍然会选择using 块。 始终为您的 IDisposable 实例设置一个只是一个好习惯。
  2. 我谦虚地建议,如果您满足上述条件,则需要重新设计您的类以利用 IDisposable 模式。

根据您发布的代码,问题是您的参数仍然存在于某个地方(也许您正在重新使用它们?)。因为参数仍然是 root 的,所以无法收集它们。它们还包含对它们所附加的命令的引用,因此您的 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 委托,而不是一个可枚举的参数。函数声明更像这样:

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-delegate 的模式并希望尽可能多地重复使用一组重复的参数,那就是拥有一个知道如何添加参数和匹配项的真实命名方法我的行动代表。然后我可以把那个方法传入,然后得到想要的参数重用。

【讨论】:

  • @Joel:其实Something就是微软自己的SqlCommand,在dispose的时候显然没有清除它的参数集合。
  • @Eduardo - 参数是可以被垃圾收集器清理的托管资源。不需要在参数集合上调用 .Clear() 。 唯一次你可能会遇到问题是你正在做一些顽皮的事情,这样参数集合仍然是根的,或者如果你在参数中存储了非常大的值,这样值就结束了在大对象堆上,即使在这种情况下,仅清除参数集合也无济于事。
  • @Eduardo - 我会给你的是 SqlDataReader,它不会被 Dispose() 方法关闭,但即使在那里,如果你依赖 CommandBehavior.CloseConnection 也只是个问题
  • @Eduardo - 好的,更新了。重要的部分是休息后的第一段。这就解释了发生了什么。
  • @Joel:您如何断定SqlDataReader 在处置时没有关闭? Dispose 方法最终肯定会调用 Close。还是说Close 方法本身并没有真正关闭阅读器?
【解决方案2】:

有趣的问题!

这完全取决于您的 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());

【讨论】:

  • 我有一个函数,它的参数是一个存储过程的名称和一个SqlParameters 数组。这些参数可以在多个存储过程调用中重用。不幸的是,SqlCommand 在处置时显然没有清除自己的参数集合。
  • 那么,您可以为每次调用该方法重新创建参数。您重用的是代码,而不是运行时参数数组。
  • 虽然典型的SqlParameter 对象不是特别重量级的对象,但如果没有必要,为什么还要创建更多呢?
  • 这是一个真正的瓶颈吗?我认为在混合使用数据库调用时,您真的不需要考虑优化应用程序的这方面。
  • 我认为这里的问题是你仍然在考虑 C++,而你应该考虑 C#。 :) 这意味着把它留给垃圾收集器来处理。
【解决方案3】:

Dispose 应该清理所有非托管资源。完全有可能使用另一种清理方法,例如执行某些功能或数据库操作。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2012-12-02
    • 2013-05-12
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-09-30
    • 1970-01-01
    • 2021-11-26
    相关资源
    最近更新 更多