不良实践?C#的using语句的非规范用法

19

C#有using语句,专门用于IDisposable对象。据推测,在using语句中指定的任何对象都将持有某种应该被确定性释放的资源。

然而,在编程中有许多设计具有单一、明确的开始和结束,但却缺乏内在的语言支持。 using结构提供了使用代码编辑器的内置功能来至少清晰自然地突出这种设计或操作范围的机会。

我所考虑的是那些经常以BeginXXX()EndXXX()方法开始的操作,尽管有许多不同的变体,例如涉及“开始”和“加入”的异步代码执行。

以这个天真的例子为例。

webDataOperation.Start();
GetContentFromHardDrive();
webDataOperation.Join();
// Perform operation that requires data from both sources

如果相反,Start方法返回一个对象,该对象的IDisposable.Dispose方法执行连接操作会怎么样。

using(webDataOperation.Start()) {
    GetContentFromHardDrive();
}
// Perform operation that requires data from both sources

更好的是,我特别想到的是:我有一个执行高度专业化图形混合的对象,并且有一个 Begin()End() 方法(这种设计也存在于DirectX和XNA中)。而不是...

using(blitter.BlitOperation()) {
    // Do work
}
// Use result

看起来更加自然、易读,但这样做是否不可取,因为它使用了IDisposable接口和using语句的非预期用途?换句话说,这是否与以一种非直观的方式重载运算符相当?


为什么不直接在开头使用 {,在结尾使用 } 呢?这样可以提供作用域而不滥用 using。 - cja
6个回答

20

这是一种完全可以接受的做法。这些被称为因式类型,框架设计准则建议采取这种方法。

基本上,如果该类型包装具有特定生命周期的操作,则使用IDisposable和using语句成为值得考虑的适当做法。

我实际上也在此处专门谈论了这个话题


不错的发现,但你有任何理由支持你的观点吗?文章推荐了它,但没有讨论为什么。 - snarf
这是因为它保证了通过Dispose()始终调用EndOperation()。 - Matthew Scharley
1
@Snarfblam:基本上,操作本身就像一个需要清理的资源。阅读我的博客文章以获取更多详细信息-这是第二个链接。我在博客中提供了比这里更多的理由。 - Reed Copsey
请注意,“using”并不“专门用于IDisposable对象”,请参阅我的关于别名泛型的文章http://ebersys.blogspot.com/2006/08/hiding-generics-complexity.html。 - BlackTigerX
这个讨论是关于using语句,而不是using指令(即导入命名空间和定义别名与该主题无关)。 - snarf

12

我不建议这样做;我的信念是,代码是用来与代码维护者有效沟通的,而不是与编译器沟通的,应该考虑到代码维护者的理解能力。我尽量只在需要丢弃资源(通常是非托管资源)时使用 "using"。

我占少数派。大多数人似乎将 "using" 作为通用的 "我想要一些清理代码即使抛出异常也会运行" 机制。

我不喜欢这个做法,因为 (1) 我们已经有了一个叫做 "try-finally" 的机制来完成这个任务,(2) 它在一个不被设计用于此目的的功能上进行了使用,(3) 如果调用清理代码很重要,那么为什么在调用它的地方看不到它呢?如果它很重要,那么我希望能够看到它。


在我看来,这种对 using 的使用方式是非常独特且受限制的。出于好奇,是否有其他 C# 语言特性您会建议不要使用,即使它能够完美清晰地运行,因为该使用方式并不是其设计目的? - Kirk Woll
@KirkWoll:是的。我建议不要使用所有语言特性来做它们设计之外的事情。 - Eric Lippert
当然,有时候很难确定一个语言特性的设计目的。实际上,即使在using这个具体案例中,我认为有一些迹象表明选择“using”这个名称是因为预计该特性不仅仅用于处理非托管资源的释放。 - kvb
我个人无法接受这种信念,因为否则使用Java在除嵌入式系统以外的任何地方都会违背你的信仰。 - rwong

6
仅仅因为你可以这样做(或者Phil Haack说可以),并不意味着你应该这样做。基本的经验法则是:如果我能读懂你的代码,理解它在做什么以及你的意图是什么,那么就是可以接受的。反之,如果你需要解释你做了什么或者为什么这样做,那么这很可能会让维护代码的初级开发人员感到困惑。还有许多其他模式可以通过更好的封装来实现这一点。最重要的是,这种“技巧”对你毫无帮助,只会使其他开发人员感到困惑。

+1 对于理解的评论。我完全同意——尽管一般情况下,我认为在许多情况下使用 IDisposable 来处理已分解的类型提供了一种非常干净、易于理解的方法……但这是编写正确代码以及非常明显地表达你正在做什么的问题。例如:BCL 中的 TransactionScope 对我来说很自然——另一个回答中的缩进代码则不然。就像许多事情一样,我认为这很有帮助,但很容易被滥用。 - Reed Copsey
同意。有效的东西就是有效的。有时候我们会被一些有趣的概念所吸引,但在实际项目中,任何只会让事情变得更加混乱的东西都不应该被使用。 - Charles
如果这对我来说看起来很混乱,我就不会问了。忽略潜在的混乱,你有没有更简洁或更简单的替代方案?我喜欢“using”是内联的、缩进的和语言支持的,明确、具体和清晰地封装作用域。 - snarf
2
@Snarfblam:在某些情况下,使用接受委托(某种形式的Action/Func)的方法,并在启动/关闭代码之间调用该方法是有效的。然而,这种方法无法处理某些情况。有关详细信息,请参见Pavel Minaev的答案。 - Reed Copsey

5

这是一种常见的模式,但个人认为,在可以使用匿名委托和/或Lambda表达式更加明显地实现相同效果的情况下,没有借口滥用IDisposable;即:

blitter.BlitOperation(delegate
{
   // your code
});

而且,如果你想的话,总是可以在另一个线程中完成工作,你不需要拆开你的代码(也就是说,这种设计增加了价值)。 - Ryan Emerle
尽管我也喜欢这种模式,但它会增加自己的复杂性——在某些情况下,它可能不如使用语句处理那么明显。许多(特别是初级)开发人员在理解 lambda 方面会有问题。 - Reed Copsey
这在某些情况下也不允许等效的安全性。例如,您无法在带有yield语句的方法内部使用此功能并获得保证的清理。 - Reed Copsey
Reed,我不确定你的意思。如果BlitOperation的主体在调用委托后进行清理,那么调用者是否为迭代器方法有什么关系呢? - Pavel Minaev

2

我认为你应该按照IDisposable的本意使用它,而不是其他用途。如果可维护性对你很重要的话。


1
John:请查看有关分解类型的框架设计指南:http://blogs.msdn.com/brada/archive/2009/02/23/framework-design-guidelines-factored-types.aspx -- 这是现在处理它们的推荐方法。 - Reed Copsey
这对可维护性有何影响?这是你建议严格遵守规范的唯一原因吗? - snarf
它会影响其他开发人员理解正在发生的事情的能力。我推荐的原因包括这样一个事实:已经很难让人们出于正常原因使用IDisposable了 - 我们不需要额外的理由。 - John Saunders
1
我同意 - 我不会(也不建议)在所有地方都使用它。 话虽如此,我认为有很好的使用情况,特别是当您必须调用关闭参数或存在副作用时。我相信这就是为什么它现在在设计指南中明确提到,并在BCL中经常以这种方式使用的原因。 在我看来,一旦它在BCL中的多个位置中使用并包含在官方设计指南中,它就成为了使用它的“正常原因”。 - Reed Copsey

0

我认为这是可以接受的 - 实际上,在一些项目中,我使用它来触发特定代码块结束时的操作。

Wes Deyer在他的LINQ to ASCII Art程序中使用了它,他称之为“action disposable”(Wes在C#编译器团队工作 - 我相信他的判断:D):

http://blogs.msdn.com/wesdyer/archive/2007/02/23/linq-to-ascii-art.aspx

class ActionDisposable: IDisposable
{
    Action action;

    public ActionDisposable(Action action)
    {
        this.action = action;
    }

    #region IDisposable Members

    public void Dispose()
    {
        this.action();
    }

    #endregion
}

现在你可以从一个函数中返回它,并像这样做:

using(ExtendedConsoleWriter.Indent())
{
     ExtendedConsoleWriter.Write("This is more indented");
}

ExtendedConsoleWriter.Write("This is less indented");

1
哇,那是滥用 using 语句。如果没有打开 ExtendedConsoleWriter 的代码,我将毫无头绪该代码的目的是什么。如果在代码周围有增加缩进和减少缩进的调用,就不会有任何问题。 - Ryan Emerle
我不会在这种情况下使用 "using",但是,假设有一个类似的关键字和相关接口,比如 "with":with(writer.Indent()){ stuff; },它将是一种方便的结构。 - snarf
3
哎呀,Ryan。那只是一个快速写出的例子。我同意,我会称其为“增加缩进” - 在这种情况下,我不会称其为“using语句的明显滥用”。这种概念可以在整个.NET中看到范围事务处理 - 你似乎对细节过于苛刻了。 - Charles

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