ADO.NET 是否过度使用 IDisposable?

Does ADO.NET go overboard with IDisposable?

当 .NET 首次出现时,我是许多抱怨 .NET 缺乏确定性终结(class 析构函数按不可预测的时间表调用)的人之一。微软当时做出的妥协是 using 声明。

虽然不完美,但我认为使用using对于确保及时清理非托管资源很重要。

但是,我正在编写一些 ADO.NET 代码并注意到几乎每个 class 都实现了 IDisposable。这导致代码看起来像这样。

using (SqlConnection connection = new SqlConnection(connectionString))
using (SqlCommand command = new SqlCommand(query, connection))
using (SqlDataAdapter adapter = new SqlDataAdapter(command))
using (SqlCommandBuilder builder = new SqlCommandBuilder(adapter))
using (DataSet dataset = new DataSet())
{
    command.Parameters.AddWithValue("@FirstValue", 2);
    command.Parameters.AddWithValue("@SecondValue", 3);

    adapter.Fill(dataset);

    DataTable table = dataset.Tables[0];
    foreach (DataRow row in table.Rows) // search whole table
    {
        if ((int)row["Id"] == 4)
        {
            row["Value2"] = 12345;
        }
        else if ((int)row["Id"] == 5)
        {
            row.Delete();
        }
    }
    adapter.Update(table);
}

强烈怀疑我不需要所有这些using声明。但是如果不详细了解每个 class 的代码,就很难确定我可以省略哪些。结果有点难看,并且有损于我代码中的主要逻辑。

有谁知道为什么所有这些 classes 都需要实施 IDisposable? (Microsoft 有许多 code examples online 无需担心处理其中许多对象。)其他开发人员是否为所有这些对象编写 using 语句?而且,如果没有,您如何决定哪些可以没有?

如果 class 实现了 IDisposable,那么您应该始终确保它得到释放,而不对其实现做任何假设。

我认为这是您可以编写的确保您的对象将被释放的最少代码量。我认为它绝对没有问题,它根本不会分散我对代码主要逻辑的注意力。

如果减少此代码对您来说至关重要,那么您可以想出自己的 SqlConnection 替代(包装器),其子 classes 未实现 IDisposable而是在连接关闭后被连接自动销毁。但是,这将是一项巨大的工作量,并且您会失去一些关于何时处置某些内容的精确度。

这里的部分问题是 ADO.NET 是一个抽象的提供者模型。我们 不知道 在处理方面需要什么特定的实现(特定的 ADO.NET 提供者)。当然,我们可以合理地假设需要处理连接和事务,但是命令呢?可能是。 Reader?可能,尤其是因为 command-flags 选项之一允许您将连接的生命周期与 reader 相关联(因此连接在 reader 关闭时关闭,这在逻辑上应该扩展到处置)。

总的来说,我觉得应该还可以。

大多数时候,人们不会手动弄乱 ADO.NET,任何 ORM 工具(或 micro-ORM 工具,例如 "Dapper")都可以做到这一点 为你不用你担心。


我会公开承认,在我使用 DataTable 的少数情况下(说真的,现在是 2018 年——除了一些小众场景外,这不应该是你表示数据的默认模型):我没有处理它们。那有点没有意义:)

是的。 ADO.Net 中的几乎所有内容都在实现 IDisposable,无论是否确实需要 - 例如 SqlConnection(because of connection pooling) or not - in case of, say, DataTable (Should I Dispose() DataSet and DataTable? ).

问题是,如问题中所述:

without understanding the code for each class in some detail, it's hard to be certain which ones I can leave out.

我认为仅此一项就足以将所有内容保存在 using 语句中 - 一句话:封装。

用很多话来说: 对于您正在使用的每个 class 的实现,没有必要非常熟悉,甚至非常熟悉。您只需要知道表面积 - 即 public 方法、属性、事件、索引器(和字段,如果 class 有 public 字段)。从 class 用户的角度来看,public 表面积以外的任何东西都是实现细节。

关于代码中的所有 using 语句 - 您可以通过创建一个接受 SQL 语句、Action<DataSet> 和参数数组的方法只编写一次它们参数。像这样:

void DoStuffWithDataTable(string query, Action<DataTable> action, params SqlParameter[] parameters)
{
    using (SqlConnection connection = new SqlConnection(connectionString))
    using (SqlCommand command = new SqlCommand(query, connection))
    using (SqlDataAdapter adapter = new SqlDataAdapter(command))
    using (SqlCommandBuilder builder = new SqlCommandBuilder(adapter))
    using (var table = new DataTable())
    {
        foreach(var param in parameters)
        {
            command.Parameters.Add(param);
        }
        // SqlDataAdapter has a fill overload that only needs a data table
        adapter.Fill(table);
        action();
        adapter.Update(table);
    }
}

你就这样使用它,用于你需要对数据执行的所有操作 table:

DoStuffWithDataTable(
    "Select...",
    table => 
    { // of course, that doesn't have to be a lambda expression here...
        foreach (DataRow row in table.Rows) // search whole table
        {
            if ((int)row["Id"] == 4)
            {
                row["Value2"] = 12345;
            }
            else if ((int)row["Id"] == 5)
            {
                row.Delete();
            }
        }
    },
    new SqlParameter[]
    {
        new SqlParameter("@FirstValue", 2),
        new SqlParameter("@SecondValue", 3)
    }
    );

这样你的代码在处理任何 IDisposable 方面是 "safe",而你只为此编写了一次管道代码。