使用IDisposable和“使用”作为获取“范围内行为”以确保异常安全的手段是否在滥用?


111

我经常在C ++中使用的一种方法是让类通过构造函数和析构函数A处理另一个类的状态进入和退出条件,以确保如果该范围内的某些事物引发了异常,则当范围已退出。就首字母缩略词而言,这并不是纯粹的RAII,但这仍然是一个既定模式。BA

在C#中,我经常想做

class FrobbleManager
{
    ...

    private void FiddleTheFrobble()
    {
        this.Frobble.Unlock();
        Foo();                  // Can throw
        this.Frobble.Fiddle();  // Can throw
        Bar();                  // Can throw
        this.Frobble.Lock();
    }
}

需要这样做

private void FiddleTheFrobble()
{
    this.Frobble.Unlock();

    try
    {            
        Foo();                  // Can throw
        this.Frobble.Fiddle();  // Can throw
        Bar();                  // Can throw
    }
    finally
    {
        this.Frobble.Lock();
    }
}

如果我想保证返回Frobble时的状态FiddleTheFrobble。该代码将更好

private void FiddleTheFrobble()
{
    using (var janitor = new FrobbleJanitor(this.Frobble))
    {            
        Foo();                  // Can throw
        this.Frobble.Fiddle();  // Can throw
        Bar();                  // Can throw
    }
}

这里FrobbleJanitor看起来大致一样

class FrobbleJanitor : IDisposable
{
    private Frobble frobble;

    public FrobbleJanitor(Frobble frobble)
    {
        this.frobble = frobble;
        this.frobble.Unlock();
    }

    public void Dispose()
    {
        this.frobble.Lock();
    }
}

这就是我想要的方式。现在现实赶上,因为我给使用要求FrobbleJanitor使用 using。我可以认为这是一个代码审查问题,但是有些困扰我。

问:将上述被视为滥用的usingIDisposable


23
+1仅用于类和方法名称:-)。好吧,公平地说,还有实际问题。
乔伊(Joey)2010年

2
@Johannes:是的,我明白了为什么-大多数人都只是在抚弄他们的小提琴,但我只是必须反对这一点。;-)而且,您的姓名相似度为+1。
Johann Gerell '01

也许您可以使用lock(this){..}范围在此示例中获得相同的结果?
2010年

1
@oɔɯǝɹ:使用lock()可以防止同时访问不同线程中的某些内容。与我描述的场景无关。
Johann Gerell '01

1
在下一个C#版本中没有终结器的IScopeable :)
乔恩·雷诺

Answers:


32

我认为不一定。IDisposable的技术上指用于事物具有非托管资源,但随后的使用指令是实现的一个常见的模式只是一种巧妙的方法try .. finally { dispose }

一个纯粹主义者会争论“是的,这是侮辱性的”,在纯粹主义者的意义上是这样。但是我们大多数人不是从纯粹主义者的角度进行编码,而是从半艺术性的角度进行编码。在我看来,以这种方式使用“使用”构造确实是非常艺术的。

您可能应该将另一个接口放在IDisposable之上,以将其进一步推开,向其他开发人员说明该接口为何暗示IDisposable。

还有许多其他选择可以做到这一点,但最终,我想不出有什么比这更简洁的了,那就去吧!


2
我相信这也是“使用”的艺术用途。我认为Samual Jack的帖子链接到Eric Gunnerson的评论,验证了“ using”的这种实现。我可以看到Andras和Eric对此提出的出色观点。
IAbstract 2010年

1
不久前,我与Eric Gunnerson进行了交谈,据他说,使用C#设计团队的意图是表示作用域。实际上,他假设在lock语句之前进行了设计,甚至可能没有lock语句。这本来是一个使用了监视监视器调用或其他功能的使用块。更新:刚刚意识到下一个答案是语言小组的埃里克·利珀特(Eric Lippert)。;)也许C#团队本身对此并不完全同意?我与Gunnerson进行的讨论的上下文是TimedLock类:bit.ly/lKugIO
Haacked 2011年

2
@Haacked-是有趣的吗?您的评论完全证明了我的观点;您曾与团队中的一个人交谈过,而他全力以赴;然后指出来自同一支球队的埃里克·利珀特(Eric Lippert)在下面表示不同意。在我看来,我的观点是; C#提供了一个利用接口的关键字,并且碰巧产生了一种在许多情况下都可以使用的漂亮代码模式。如果我们不应该滥用它,那么C#应该找到另一种方法来实施它。无论哪种方式,设计师都可能需要在这里连续放鸭!同时:using(Html.BeginForm())先生?先生,请不要介意!:)
Andras Zoltan

71

我认为这是对using语句的滥用。我知道我在这个职位上只是少数。

我认为这是一种滥用,原因有三点。

首先,因为我希望使用“ using”来使用资源在完成使用后对其进行处置。更改程序状态不使用资源,而将其更改回不处理任何东西。因此,“使用”来突变和恢复状态是一种滥用。该代码会误导普通读者。

第二,因为我期望出于礼貌而非必要而使用“使用” 。使用完文件后使用“使用”处理文件的原因不是因为有要这样做,而是因为它很礼貌 -可能有人在等待使用该文件,所以说“完成”现在”是道义上正确的事情。我希望我应该能够重构“使用”,以便将使用过的资源保留更长的时间,并在以后处置,并且这样做的唯一影响是给其他流程带来一些不便对程序状态语义影响的 “使用”块 之所以滥用它,是因为它在构造中隐藏了程序状态的重要且必要的突变,而这种构造看起来是为了方便和礼貌而不是必要。

第三,程序的动作由状态决定。正是需要对状态进行仔细操纵的原因恰恰是我们首先进行此对话的原因。让我们考虑一下我们如何分析您的原始程序。

您是否要在我的办公室将其带入代码审查中,我要问的第一个问题是:“如果引发异常,锁定Frobble真的正确吗?” 从您的程序中可以很明显地看出,无论发生什么事情,这个东西都会积极地重新锁定Frobble。那正确吗? 引发了异常。程序处于未知状态。我们不知道Foo,Fiddle或Bar是否扔了,为什么扔了,或者对未清除的其他状态进行了哪些突变。您能说服我,在这种可怕的情况下,重新锁定始终是正确的选择吗?

也许是,也许不是。我的观点是,使用最初编写的代码代码审阅者知道要提出问题了。使用使用“ using”的代码,我不知道问这个问题。我假设“使用”块分配了一个资源,使用了一段时间,并在完成后有礼貌地处置了它,而不是假设“使用”块的结尾括号在任意数量的情况下会在异常情况下改变我的程序状态程序状态一致性条件已被违反。

使用“ using”块具有语义效果使该程序片段:

}

非常有意义。当我查看单个大括号时,我不会立即想到“大括号具有对程序的整体状态产生深远影响的副作用”。但是,当您这样滥用“使用”时,突然会发生。

我会问我是否看到您的原始代码的第二件事是“如果在解锁后但在尝试输入之前引发了异常,会发生什么情况?” 如果您正在运行未优化的程序集,则编译器可能在尝试之前插入了无操作指令,并且有可能在无操作上发生线程中止异常。这种情况很少见,但确实会在现实生活中发生,尤其是在Web服务器上。在这种情况下,解锁会发生,但锁定永远不会发生,因为在尝试之前就抛出了异常。此代码很可能容易受到此问题的影响,因此应实际编写

bool needsLock = false;
try
{
    // must be carefully written so that needsLock is set
    // if and only if the unlock happened:

    this.Frobble.AtomicUnlock(ref needsLock);
    blah blah blah
}
finally
{
    if (needsLock) this.Frobble.Lock();
}

再一次,也许是,也许不是,但是我知道问这个问题。对于“使用”版本,它容易遇到相同的问题:在锁定Frobble之后但在进入与using相关联的try-protected区域之前,可能会引发线程异常中止。但是,对于“使用”版本,我认为这是“那又怎样?” 情况。不幸的是,这种情况发生了,但是我认为“使用”只是为了礼貌,而不是改变至关重要的程序状态。我假设,如果在错误的时间发生了一些可怕的线程中止异常,那么,好吧,垃圾收集器最终将通过运行终结器来清理该资源。


18
尽管我会以不同的方式回答这个问题,但我认为这是一个有争议的帖子。但是,我对您所说using的礼貌而非正确性表示质疑。我相信资源泄漏是正确性问题,尽管它们通常很小,以至于它们不是高优先级的错误。因此,using已经对程序状态产生了语义影响,它们所防止的不仅是对其他进程的“不便”,而且还可能破坏了环境。这并不是说问题所暗示的不同类型的语义影响也是适当的。
kvb

13
资源泄漏是正确性问题,是的。但是,如果不使用“使用中”,则不会泄漏资源。终结器运行时,最终将清除资源。清理可能没有您想要的那么积极和及时,但它确实会发生。
埃里克·利珀特

7
精彩的帖子,这是我的两分钱:)我怀疑很多“滥用” using是因为这是C#中一个穷人面向方面的编织者。开发人员真正想要的是一种确保某些通用代码将在块的末尾运行而不必重复该代码的方法。C#今天不支持AOP(不支持MarshalByRefObject和PostSharp)。但是,编译器对using语句的转换使得可以通过重新利用确定性资源处理的语义来实现简单的AOP。虽然这不是一个好习惯,但有时会吸引人..
LBushkin

11
虽然我通常同意您的立场,但在某些情况下,重新设定目标的利益using太诱人而无法放弃。我能想到的最好的示例是跟踪和报告代码计时:using( TimingTrace.Start("MyMethod") ) { /* code */ } 同样,这是AOP- Start()捕获块开始之前的开始时间,Dispose()捕获结束时间并记录活动。它不会更改程序状态,并且与异常无关。它也很有吸引力,因为它避免了很多麻烦,并且作为一种模式来减轻混乱的语义,因此经常使用。
LBushkin

6
有趣的是,我一直认为使用它作为支持RAII的一种手段。对于我来说,将锁视为一种资源似乎很直观。
布赖恩2010年

27

如果您只需要一些干净的,有范围的代码,则也可以使用lambdas,

myFribble.SafeExecute(() =>
    {
        myFribble.DangerDanger();
        myFribble.LiveOnTheEdge();
    });

其中.SafeExecute(Action fribbleAction)方法包装try- catch- finally块。


在该示例中,您错过了输入状态。此外,一定要在包装最后的try /最后,但你不叫什么,所以如果事情在lambda抛出你不知道你现在的状态。
约翰Gerell

1
啊! 好吧,我想你的意思是SafeExecuteFribble它调用方法Unlock(),并Lock()在包裹尝试/最后。那我道歉。我立即将其SafeExecute视为通用扩展方法,因此提到缺少进入和退出状态。我的错。
Johann Gerell '01

1
请谨慎使用此方法。对于被捕获的本地人,可能会有一些危险的微妙的,意想不到的寿命延长!
杰森

4
育。为什么隐藏try / catch / finally?隐藏您的代码以供他人阅读。
Frank Schwieterman

2
@Yukky Frank:“隐藏”任何东西不是我的主意,这是发问者的要求。:-P ...也就是说,请求最终是“不要重复自己”的问题。您可能有很多方法都需要使用相同的样板“框架”来干净地获取/释放某些内容,而又不想将其强加给调用者(想想封装)。也可以更清晰地命名诸如.SafeExecute(...)之类的方法,以充分传达其功能。
herzmeister 2010年

24

C#语言设计团队的埃里克·冈纳森(Eric Gunnerson)对几乎相同的问题给出了以下答案

道格问:

回复:带超时的锁语句...

在处理多种方法中的通用模式之前,我已经完成了这一技巧。通常锁定获取,但还有其他一些。问题在于,它总是像黑客一样,因为该对象并不是真正的一次性对象,而是“ 在范围尽头的回调 ”。

道格

当我们决定using语句时,我们决定将其命名为“ using”,而不是更具体的对象放置方式,以便可以将其用于这种情况。


4
好吧,那句话应该说出他所说的“正是这种情况”时指的是什么,因为我怀疑他指的是这个未来的问题。
Frank Schwieterman

3
@弗兰克·施维特曼:我完成了报价。显然,来自C#团队的人们确实认为该using功能不仅限于资源处理。
paercebal

11

这是一个湿滑的斜坡。IDisposable拥有一份合同,该合同由终结者备份。在您的情况下,终结器是没有用的。您不能强迫客户使用using语句,而只能鼓励客户这样做。您可以使用以下方法强制使用:

void UseMeUnlocked(Action callback) {
  Unlock();
  try {
    callback();
  }
  finally {
    Lock();
  }
}

但是,如果没有lamda,这会变得有些尴尬。也就是说,我像您一样使用IDisposable。

但是,您的帖子中有一个细节,使它很危险地接近反模式。您提到那些方法可以引发异常。这不是呼叫者可以忽略的东西。他可以为此做三件事:

  • 不执行任何操作,该异常不可恢复。正常情况。调用解锁无所谓。
  • 捕获并处理异常
  • 在他的代码中恢复状态,并让异常沿调用链传递。

后两个要求调用者显式编写一个try块。现在,using语句成为障碍。这很可能使客户陷入昏迷,使他相信您的班级正在照顾国家,无需做任何其他工作。这几乎永远都不准确。


我认为,强迫案件是由上述“ herzmeister der welten”提出的,即使OP似乎不喜欢这个例子。
本杰明·波兹尊

是的,我将他解释SafeExecute为一种通用扩展方法。我现在看到,它很可能是作为一种Fribble方法调用的,Unlock()并且Lock()在包装好的try / final中。我的错。
Johann Gerell '01

忽略异常并不意味着它不可恢复。这意味着它将在堆栈中的上方进行处理。例如,可以使用异常来正常退出线程。在这种情况下,您必须正确释放资源,包括锁定的锁。
paercebal

7

一个真实的例子是ASP.net MVC的BeginForm。基本上你可以这样写:

Html.BeginForm(...);
Html.TextBox(...);
Html.EndForm();

要么

using(Html.BeginForm(...)){
    Html.TextBox(...);
}

Html.EndForm调用Dispose,而Dispose仅输出</form>标记。这样做的好处是{}括号创建了一个可见的“范围”,使查看表单中的内容和不包含内容的内容更加容易。

我不会过度使用它,但是从本质上讲,IDisposable只是说“完成后必须调用此函数”的一种方式。MvcForm使用它来确保关闭窗体,Stream使用它来确保关闭流,您可以使用它来确保对象被解锁。

就个人而言,只有在满足以下两个规则的情况下,我才会使用它,但我会随意设置它们:

  • Dispose应该是一个始终必须运行的函数,因此除了Null-Checks之外不应有任何条件
  • 在Dispose()之后,该对象不应重新使用。如果我想要一个可重用的对象,我宁愿给它打开/关闭方法,而不是处置。因此,当尝试使用已处置的对象时,我抛出了InvalidOperationException。

最后,一切都与期望有关。如果对象实现IDisposable,我认为它需要进行一些清理,因此我将其称为。我认为通常比具有“关闭”功能更好。

话虽如此,我不喜欢这条线:

this.Frobble.Fiddle();

当FrobbleJanitor现在“拥有” Frobble时,我想知道代替Fiddle作为看门人中的Frobble会更好吗?


关于您的“我不喜欢这条线”-实际上,我在提出问题时曾短暂地想到过这样做,但是我不想弄乱我的问题的语义。但我有点同意你的看法。有点儿。:)
Johann Gerell

1
赞成从Microsoft本身提供一个示例。请注意,在您提到的情况下,有一个特定的异常被抛出:ObjectDisposedException。
Antitoon

4

我们在代码库中大量使用了这种模式,以前我在所有地方都看到过这种模式-我确信在这里也已经讨论过了。总的来说,我看不出这样做有什么问题,它提供了一种有用的模式,并且没有造成实际伤害。


4

对此表示赞同:我在这里大部分人都同意这是脆弱的,但很有用。我想将您指向System.Transaction.TransactionScope类,该类可以完成您想要的操作。

通常,我喜欢这种语法,它消除了很多真实的肉。请考虑给辅助类一个好名字-也许...范围,如上面的示例。该名称应该放弃它封装了一段代码。*范围,*块或类似的东西应该做。


1
“看门人”来自Andrei Alexandrescu的一篇有关C ++防护的老C ++文章,甚至在ScopeGuard进入Loki之前。
Johann Gerell's

@Benjamin:我喜欢TransactionScope类,之前从未听说过。也许是因为我在.Net CF领域中,但不包括在内... :-(
Johann Gerell,2010年

4

注意:我的观点可能与我的C ++背景有所偏差,因此应针对可能的偏差评估我的答案的值...

什么是C#语言规范?

引用C#语言规范

8.13 using语句

[...]

资源是一个类或结构实现System.IDisposable的,它包括一个名为处置单个参数方法。使用资源的代码可以调用Dispose来指示不再需要该资源。如果未调用Dispose,则最终将由于垃圾回收而自动进行处置。

正在使用的资源的代码,当然是,代码开始由using关键字和下去,直到附连到的范围using

所以我想这是可以的,因为Lock是一种资源。

也许关键字using选择不正确。也许应该调用它scoped

然后,我们可以将几乎所有内容视为资源。文件句柄。网络连接...线程?

一个线程???

使用(或滥用) using关键字?

在退出范围之前使用(ab)关键字来确保线程的工作已结束会很闪亮using吗?

Herb Sutter似乎认为它很闪亮,因为他对IDispose模式进行了有趣的使用,以等待线程的工作结束:

http://www.drdobbs.com/go-parallel/article/showArticle.jhtml?articleID=225700095

这是从文章复制粘贴的代码:

// C# example
using( Active a = new Active() ) {    // creates private thread
       
       a.SomeWork();                  // enqueues work
       
       a.MoreWork();                   // enqueues work
       
} // waits for work to complete and joins with private thread

虽然没有提供Active对象的C#代码,但C#使用IDispose模式编写其C ++版本包含在析构函数中的代码。通过查看C ++版本,我们看到了一个析构函数,该析构函数在退出之前等待内部线程结束,如本文的其他摘录所示:

~Active() {
    // etc.
    thd->join();
 }

因此,就Herb而言,它是有光泽的


3

我相信您的问题的答案是否定的,这不会滥用IDisposable

我了解该IDisposable接口的方式是,一旦对象被处置,就不应使用它(除非您可以随意调用它的Dispose方法)。

由于您创建了 FrobbleJanitor每次访问该using语句时都会显式FrobbeJanitor对象,因此您永远不会重复使用同一对象。由于它的目的是管理另一个对象,Dispose似乎适合于释放此(“托管”)资源的任务。

(顺便说一句,证明Dispose几乎总是正确执行的标准示例代码表明,也应该释放托管资源,而不仅仅是文件系统句柄之类的非托管资源。)

我个人担心的唯一一件事是,与直接显示&操作using (var janitor = new FrobbleJanitor())的更明确的try..finally块相比,它所发生的事情还不清楚。但是,采用哪种方法可能取决于个人喜好。LockUnlock


1

它不是虐待。您正在使用它们创建它们的目的。但是您可能必须根据自己的需要考虑彼此。例如,如果您选择“ artistry”,则可以使用“ using”,但是如果您的代码段执行了很多次,则出于性能原因,您可以使用“ try” ..“ finally”构造。因为“使用”通常涉及对象的创建。


1

我认为您做对了。重载Dispose()将是一个问题,同一类后来必须进行清理,并且清理的生存期更改为不同于您希望持有锁的时间。但是,由于您创建了一个单独的类(FrobbleJanitor),该类仅负责锁定和解锁Frobble,因此事情已经解耦了很多,您将不会遇到该问题。

我将重命名为FrobbleJanitor,大概是FrobbleLockSession之类的。


关于重命名-我使用“管理员”来处理锁定/解锁的主要原因。我认为它与其他名称选择押韵。;-)如果要在整个代码库中将此方法用作模式,则一定会包含一些更通用的描述性内容,如您所暗示的“ Session”。如果这是过去的C ++年代,我可能会使用FrobbleUnlockScope。
Johann Gerell '01
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.