我应该多快结束using代码块?

4
有一天在代码审查中,出现了一个关于使用块应该何时关闭的问题。一派认为“当你完成对象操作后”,另一派则认为“在超出其作用域之前的某个时刻”。
在这个具体的例子中,有一个需要被处理的 DataTable 和 SqlCommand 对象。我们需要在单个语句中引用两者,并且需要迭代 DataTable。
第一派:
List<MyObject> listToReturn = new List<MyObject>();
DataTable dt = null;
try
{
    using (InHouseDataAdapter inHouseDataAdapter = new InHouseDataAdapter())
    using (SqlCommand cmd = new SqlCommand())
    {
        dt = inHouseDataAdapter.GetDataTable(cmd);
    }

    foreach (DataRow dr in dt.Rows)
    {
        listToReturn.Add(new MyObject(dr));
    }
}
finally
{
    if (dt != null)
    {
        dt.Dispose();
    }
}

原因:使用完SqlCommand后立即处理掉它。不要在另一个对象的using块内开始可能会很长的操作,比如迭代表。

阵营2:

List<MyObject> listToReturn = new List<MyObject>();
using (InHouseDataAdapter inHouseDataAdapter = new InHouseDataAdapter())
using (SqlCommand cmd = new SqlCommand())
using (DataTable dt = inHouseDataAdapter.GetDataTable(cmd))
{
    foreach (DataRow dr in dt.Rows)
    {
        listToReturn.Add(new MyObject(dr));
    }
}

推理:这段代码更加干净。无论如何,所有对象都有保证被处理,并且没有真正的资源密集型,因此立即处理任何对象并不重要。
我属于第二派。你呢?为什么?
编辑:一些人指出DataTable不需要被处理(参见Corey Sunwold's answer),而且第一派最初的示例比它实际需要的更糟糕。以下是一些修改后的示例,也考虑到大多数情况下,我必须在SqlCommand上设置一些属性。如果有人看到或想到支持任何一方的更好的例子,请分享。
第一派,版本2:
DataTable dt = null;
using (InHouseDataAdapter inHouseDataAdapter = new InHouseDataAdapter(_connectionString))
using (SqlCommand cmd = new SqlCommand("up_my_proc"))
{
    cmd.CommandType = CommandType.StoredProcedure;
    cmd.Parameters.Add("@class_id", 27);
    dt = inHouseDataAdapter.GetDataTable(cmd);
}

foreach (DataRow dr in dt.Rows)
{
    listToReturn.Add(new MyObject(dr));
}

营地2,版本2:

using (InHouseDataAdapter inHouseDataAdapter = new InHouseDataAdapter(_connectionString))
using (SqlCommand cmd = new SqlCommand("up_my_proc"))
{
    cmd.CommandType = CommandType.StoredProcedure;
    cmd.Parameters.Add("@class_id", 27);
    DataTable dt = inHouseDataAdapter.GetDataTable(cmd);
    foreach (DataRow dr in dt.Rows)
    {
        listToReturn.Add(new MyObject(dr));
    }
}

我认为大多数人都会同意,可读性的论点现在已经大大降低,而这并不是我试图询问的最好的例子。(特别是一旦我告诉你,在GetDataTable()方法退出之前SqlConnection已关闭,并且对于此实例中使用的数据没有可衡量的性能差异。)如果我可以晚些时候补充我的问题,是否有情况下立即处理对象确实会有所区别?例如,像Gregory Higley提到的共享资源,如操作系统句柄。

编辑:(解释我的答案选择)非常感谢所有提供意见、示例和其他有用反馈的人!我们似乎分歧差不多,但所有人的回答都强调了一个观点,“阵营1绝对是正确的,但根据物品的不同,阵营2也许可以接受”。我本意是讨论所有类型物品的处理方式,但我选择了一个不好的例子来说明。由于讨论的大部分重点放在了那个特定的例子上,我选择了那个给我提供了关于具体使用对象重要信息,并证明我需要仔细考虑每个对象的决策的答案。 (对于我的标题这样模糊的问题,选择“最佳答案”将很困难。)未来遇到相同困境的读者,请看下面的所有答案,因为它们中的许多都提出了有趣的观点。

评论者有点过于热心了 - 除非DataTable有大量行,否则这并不重要。最重要的是您确实正在处理对象并且应用程序表现良好(当然代码可读性也很重要)。 - Greg Sansom
Camp 2 FTW!这就是15。 - Marcote
在这两个例子之间,我也会选择Camp 2,但考虑到@Corey Sunwold的答案,你可以摆脱Camp 1答案中的“try finally”,它看起来非常干净。另外,在这两种情况下,我认为你应该在“foreach”之前检查“dt!= null”。 - Jeff Ogata
@adrift 是的,考虑到 Corey 的观点,我觉得问题现在有些误导人了。正如一个回答者所说,整个例子似乎是刻意编造的(尽管实际上并非如此),而 Camp 1 的版本则是对“早释放”论点的“稻草人”攻击。既然已经有这么多答案了,我应该编辑示例吗?还是添加修订后的示例? - Kimberly
@adrift 我考虑过加入 null 检查以保证完整性,但这会使每个答案都变得复杂,所以出于简洁起见我将其省略了。在我们的情况下,由于 GetDataTable() 方法的实现方式,dt 永远不会为 null:如果无法返回数据,它会抛出异常。我认为这不是正确的行为,但我知道它不会改变,因为许多应用程序现在依赖于它。 - Kimberly
一旦您完成了该对象,+1。 - Kieren Johnstone
9个回答

3

有趣。我以前从未见过这个,所以感谢你指出来。我的例子是真实的,而不是人为制造的,所以我们的困境通过这个得到了解决。但我想可能还有其他类型的对象,即使立即处理也会让代码变得丑陋。我们将在其他答案中看到结果。 - Kimberly
@Kimberly 我很高兴看到人们真正利用块。你不知道我看过多少 C# 代码,写它的人不理解 IDisposable 对象是什么或者为什么 Dispose 很重要。 - Corey Sunwold

1

我认为这取决于可处理对象所持有的非托管资源类型。总的来说,我和你一样属于第二派。

如果非托管资源以某种方式共享,特别是如果它代表某种操作系统句柄,我会尽快处理它。


0

支持Camp 2。

您的Camp 1示例处置了可能在被处理后仍可使用的对象。我认为这不是您的具体情况的问题,但可能会在其他情况下引起问题。Camp 2版本强制您在正确嵌套的作用域中处置对象,使代码更加安全。

示例:

StreamWriter writer;
using(var stream = new FileStream(name))
{
    writer = new StreamWriter(stream);
}
writer.Write(1); // <= writnig to closed stream here.
writer.Dispose();

谢谢。这似乎是一个重要的观点,尽管到目前为止只有你提出了这个观点。我认为我的问题值得问,但我的例子不太好。如果我知道 DataTable 不需要被处理,我会使用更好的例子 - 也许像这样。除了在这种情况下,这不是哪种方式更好的问题:这段代码永远不合法。如果阵营1支持它,他们绝对是错误的,而不仅仅是“不太正确”。 - Kimberly

0

你的Camp 1示例有点假象,它不必那么丑陋。

如果性能是一个问题(这可能很少见,在这个人为的示例中不太可能出现),我会通过重构生成DataTable的方法来采用更干净的“Camp 1”版本:

private DataTable GetDataTableFromInHouseAdapter()
{
    using (InHouseDataAdapter inHouseDataAdapter = new InHouseDataAdapter())
    using (SqlCommand cmd = new SqlCommand())
    {
        return inHouseDataAdapter.GetDataTable(cmd);
    }
}

...
List<MyObject> listToReturn = new List<MyObject>();
using (DataTable dt = GetDataTableFromInHouseAdapter())
{
    foreach (DataRow dr in dt.Rows)
    {
        listToReturn.Add(new MyObject(dr));
    }
}

这看起来更为现实 - 生成DataTable的方法应该属于数据访问层,将其转换成MyObject实例列表的方法可能应该放在DAL之上的外观中。

事实上,我总是会考虑将嵌套的using语句重构为自己的方法 - 除非它们密切相关(如SqlConnection / Command或甚至InHouseDataAdapter / SqlCommand)。


我同意关于“草人”的观点。信不信由你,这个例子是真实的,而不是刻意编造的,但如果我知道 DataTable 不需要被处理,我也不会使用它。 (我会想出更公平的东西。)我只是不确定此时是否适合大幅编辑问题。 - Kimberly
我也更喜欢这种方法结构,但是我的组织标准的设计模式没有这么多的抽象层。我可以将这两个方法放在同一个类中 - 假装有一个额外的层 - 但除非内部方法至少有两个调用者,否则我很可能会被告知将它们拼回一起。 - Kimberly
“...被告知将它们重新组合在一起,除非内部方法至少有两个调用者” - 这只是病态的,我不认为我能在这样的环境中长久生存 :) - Joe

0
我和你一样在第二个阵营。有时候资源的重要性是由机器上可用的资源决定的。第二个阵营的解决方案相信采取预防措施,即在完成后删除对象,而不是像懒惰的第一个阵营那样。

0

通常我们选择第二种方案 - 因为外部使用语句使用了 SqlTransaction 对象。

我们需要在结束时处理事务,以便如果在使用数据读取器时抛出异常,则可以回滚事务。


0

Camp2更易读,所以在大多数情况下我会选择它而不是Camp1。你的代码越易读,支持它时就会越少痛苦。

在一些罕见的情况下,如果需要非常快速地处理资源,我会选择Camp1。我的意思是,如果稍后处理连接遇到了一些问题。但在大多数情况下,如果你采用Camp2的方式,就不会有任何惩罚。


0

一切都取决于您认为保留外部资源多长时间是合理的。数据连接并非免费,因此尽快清理它们是有意义的。为了可读性,我仍然会坚持我的立场,因为这里的资源保留对象非常清晰。

但实际上,这是一个有点纯粹的问题,因为我们在这里谈论的是低毫秒级别。我会说倾向于第一种情况,特别是如果您长时间使用查询的数据。

此外,我会摆脱DataTable的清理。对于没有资源引用的托管对象,这是不必要的。这样做基本上否定了可读性的论点。


我修改了代码示例以使阵营1得到更公正的代表。总的来说,我选择了一个不好的例子,但我仍然收到了很多好的反馈,所以谢谢你! - Kimberly

0

另一个可能的设计是在它们的“Using”块中关闭inHouseDataAdapter或SqlCommand,因为需要正确的IDisposable实现来容忍多次清理尝试。在许多情况下,我认为在Using块内显式调用Close / Dispose可能是一个好主意,因为显式调用的Close方法可能提供比IDisposable.Dispose更有帮助的异常(反对IDisposable.Dispose抛出异常的论点不适用于Close方法)。

在这种特定情况下,我会肯定地将SqlCommand和inHouseDataAdapter保持打开状态,同时将DataTable复制到List中。如果GetDataTable返回实际包含数据的DataTable,则foreach循环应该快速执行(因此数据提供实体不会被过度长时间保持打开)。只有在它返回懒惰读取的DataTable派生类时,foreach循环才需要花费一些时间来执行,在这种情况下,数据提供实体可能需要保持足够长的时间以完成循环。


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