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

时间:2010-01-20 13:13:48

标签: c# exception-handling raii

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

在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

12 个答案:

答案 0 :(得分:68)

我认为这是滥用使用声明。我知道我在这个职位上占少数。

我认为这是一种滥用行为有三个原因。

首先,因为我希望“使用”用于使用资源并且在您完成它时处理它。更改程序状态不是使用资源并且更改它不是处理任何东西。因此,“使用”变异和恢复状态是一种滥用;这段代码误导了随意的读者。

其次,因为我希望“使用”出于礼貌使用,而不是必需。你完成它后使用“使用”处理文件的原因不是因为必要这样做,而是因为它是礼貌的 - 其他人可能正在等待使用该文件,所以说“现在完成”是道德上正确的事情。我希望我能够重构“使用”,以便将使用过的资源保留更长时间,并在以后处理,并且这样做的唯一影响是稍微给其他进程带来不便 。对程序状态具有语义影响的“使用”块是滥用的,因为它隐藏了一个重要的,需要的程序状态突变,它看起来像是为了方便和礼貌,而不是必需。< / p>

第三,你的计划的行动是由其国家决定的;谨慎操纵国家的必要性正是我们首先进行这种对话的原因。让我们考虑一下如何分析原始程序。

如果你把它带到我办公室的代码审查中,我要问的第一个问题是“如果抛出异常,锁定欺骗是否真的正确?”从你的程序中可以明显地看出,无论发生什么事情,这件事都会积极地重新锁定。 是吗?抛出异常。该程序处于未知状态。我们不知道Foo,Fiddle或Bar是否扔了,他们为什么扔了,或者他们对其他没有清理的状态发生了什么突变。你可以说服我在那种可怕的情况下总是重新锁定正确的事情吗?

也许是,也许不是。我的观点是,使用最初编写的代码,代码审阅者知道提出问题。使用“使用”的代码,我不知道问这个问题;我假设“using”块分配一个资源,稍微使用它,并在完成后礼貌地处理它,而不是“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被锁定之后但在输入与使用相关联的尝试保护区域之前,可能抛出线程中止异常。但是使用“使用”版本,我认为这是“那么什么?”情况。不幸的是,如果发生这种情况,但我认为“使用”只是礼貌,而不是改变极其重要的程序状态。我假设如果在错误的时间发生了一些可怕的线程中止异常,那么,好吧,垃圾收集器最终将通过运行终结器来清理该资源。

答案 1 :(得分:31)

我不这么认为,必然。 IDisisableable技术上 意味着用于具有非托管资源的东西,但是using指令只是实现try .. finally { dispose }的公共模式的一种巧妙方式。

纯粹主义者会争辩说“是的 - 它是滥用的”,而在纯粹主义意义上它是;但我们大多数人不是从纯粹的角度编码,而是从半艺术角度编码。在我看来,以这种方式使用“使用”结构确实非常具有艺术性。

你可能应该在IDisposable上添加另一个接口以将其推得更远一些,向其他开发人员解释为什么该接口意味着IDisposable。

还有很多其他的选择,但最终,我想不出任何会像这样整洁,所以去吧!

答案 2 :(得分:26)

如果你只想要一些干净的范围代码,你也可以使用lambdas,ála

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

.SafeExecute(Action fribbleAction)方法包含try - catch - finally块。

答案 3 :(得分:24)

参与C#语言设计团队的Eric Gunnerson对this answer提出了几乎相同的问题:

  道格问道:

     
    
      

re:带超时的锁定语句......

             

在使用多种方法处理常见模式之前,我已经完成了这个技巧。通常锁定获取,但有其他一些。问题是它总是感觉像是一个黑客,因为对象实际上不是一次性的,而是“回调最后一个范围的”。

    
  
     

道格

     

当我们决定[sic] using语句时,我们决定将其命名为“using”,而不是更具体地处理对象,以便它可以用于这种情况。

答案 4 :(得分:11)

这是一个滑坡。 IDisposable有一份合同,一份由终结者支持的合同。在你的情况下,终结器是没用的。您不能强制客户端使用using语句,只能鼓励他这样做。您可以使用以下方法强制它:

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

但如果没有lamdas,这往往会有点尴尬。也就是说,我像你一样使用了IDisposable。

然而,你的帖子中有一个细节,使得它非常接近反模式。您提到这些方法可以抛出异常。这不是呼叫者可以忽略的。他可以做三件事:

  • 什么都不做,例外是不可恢复的。正常情况。调用解锁并不重要。
  • 抓住并处理异常
  • 在他的代码中恢复状态,让异常传递给调用链。

后两者要求调用者显式写一个try块。现在,using语句会受到妨碍。它可能会诱使客户陷入昏迷状态,使他相信你的班级正在照顾国家,而且不需要做额外的工作。这几乎从来都不准确。

答案 5 :(得分:6)

真实世界的例子是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,我想知道在看门人的Frobble上打电话给Fiddle是不是更好?

答案 6 :(得分:4)

我们在代码库中充分利用了这种模式,之前我已经看过它 - 我相信它一定也在这里讨论过。一般来说,我没有看到这样做有什么问题,它提供了一个有用的模式,并没有造成真正的伤害。

答案 7 :(得分:4)

明白这一点:我同意大多数人认为这是脆弱的,但很有用。我想指出System.Transaction.TransactionScope课程,它会做你想做的事情。

一般来说,我喜欢这种语法,它会从真正的肉中消除很多混乱。请考虑给助手类一个好名字 - 也许......范围,就像上面的例子。该名称应该表明它封装了一段代码。 *范围,*块或类似的东西应该这样做。

答案 8 :(得分:4)

注意:我的观点可能偏离了我的C ++背景,所以应该根据可能的偏见评估我的答案的价值......

C#语言规范是什么意思?

引用C# Language Specification

  

8.13使用声明

     

[...]

     

资源是实现System.IDisposable的类或结构,它包含一个名为Dispose的无参数方法。使用资源的代码可以调用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而言,它是闪亮的

答案 9 :(得分:3)

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

我理解IDisposable接口的方式是,一旦处理完对象,你不应该使用它(除了你可以像你一样经常调用它的Dispose方法要)。

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

(顺便说一下,演示Dispose正确实现的标准示例代码几乎总是表明应该释放托管资源,而不仅仅是文件系统句柄等非托管资源。)

我个人唯一担心的是,using (var janitor = new FrobbleJanitor())发生的事情与try..finallyLock&amp; Unlock操作直接可见。但采取哪种方法可能归结为个人偏好。

答案 10 :(得分:1)

它不是滥用。您正在使用它们创建的内容。但是你可能需要根据你的需要相互考虑。例如,如果您选择“艺术性”,那么您可以使用“使用”,但如果您的代码被执行了很多次,那么出于性能原因,您可以使用'try'...'finally'构造。因为“使用”通常涉及对象的创作。

答案 11 :(得分:1)

我认为你做得对。重载Dispose()将是一个问题,同一个类后来实际上必须进行清理,并且该清理的生命周期改变为与您希望持有锁的时间不同。但是既然你创建了一个单独的类(FrobbleJanitor),它只负责锁定和解锁Frobble,那么事情就会解耦,你就不会遇到这个问题。

我会重命名FrobbleJanitor,可能会像FrobbleLockSession那样。