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

我经常在C ++中使用的东西是让A类通过A构造函数和析构函数处理另一个类B的状态进入和退出条件,以确保如果该范围内的某个东西抛出一个exception,那么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(); } } 

如果我想在FiddleTheFrobble返回时保证Frobble状态。 代码会更好

 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 ,因为我想要使用FrobbleJanitorusing 。 我可以认为这是一个代码审查的问题,但有些事情是唠叨我。

问题:以上是否被认为是滥用和using IDisposable

我不这么认为。 从技术上来说,IDisposable 为了用于那些有非托pipe资源的事情,但是然后using指令只是实现try .. finally { dispose }一个通用模式的一种简洁的方式try .. finally { dispose }

一个纯粹主义者会认为“是的 – 这是滥用的”,从纯粹的意义上来说, 但我们大多数人不是从纯粹的angular度来编码,而是从半艺术的angular度来编码。 在我看来,以这种方式使用“使用”结构确实颇具艺术性。

你也许应该在IDisposable之上再加一个接口,把它推得更远,向其他开发者解释为什么这个接口意味着IDisposable。

除此之外,还有很多其他的select,但最终我想不出任何这样的东西,所以去做吧!

我认为这是滥用使用声明。 我知道我在这个位置上是less数。

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

首先,因为我期望“使用”用于使用资源在完成时处理它 。 改变程序状态不是使用资源 ,改变它不是处置任何东西。 因此,“使用”变异和恢复状态是一种滥用; 该代码是误导给不经意的读者。

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

第三,你的程序的行为是由其状态决定的; 仔细操纵国家的需要正是我们为什么要首先进行这种对话的原因。 让我们考虑一下如何分析你的原始程序。

如果你把这个提交到我的办公室进行代码审查,我会问的第一个问题是“如果抛出exception,locking垃圾邮件真的是正确的吗? 从你的程序中明显看出,无论发生什么事情,这个事情都会积极地重新locking可能。 是对的吗? 抛出exception。 该程序处于未知状态。 我们不知道Foo,Fiddle或Bar是否投掷,他们为什么投掷,或者他们对其他国家进行了什么样的突变,而这些突变没有被清理。 你能否说服我,在这种可怕的情况下,重新locking总是正确的?

也许是,也许不是。 我的观点是,在原来的代码中代码审查人员知道要问这个问题 。 用“使用”的代码,我不知道问这个问题; 我假设“使用”块分配一个资源,使用它一点点,当它完成时礼貌地处理它,而不是在“使用”块的最后大括号 突变我的程序状态在特殊的情况下,当任意多程序状态一致性条件已被违反。

使用“使用”块来产生语义效果使得该程序片段:

 } 

非常有意义。 当我看到那个单一的紧箍咒时,我不会立刻想到“这个支架有副作用,对我的程序的全球状态有深远的影响”。 但是,当你这样滥用“使用”时,它会突然发生。

我会问,如果我看到你的原始代码的第二件事是“如果在解锁之后但在input尝试之前抛出exception会发生什么? 如果你正在运行一个未经过优化的程序集,那么编译器可能在try之前插入了一个no-op指令,在no-op中可能会发生线程中止exception。 这是很less见的,但它确实发生在现实生活中,特别是在Web服务器上。 在这种情况下,解锁发生,但永远不会发生locking,因为在尝试之前抛出exception。 这个代码完全有可能被这个问题所困扰,而且实际上应该被写入

 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被locking之后,但在与使用相关联的尝试保护区域被input之前,可能会抛出线程exception终止exception。 但是对于“使用”版本,我认为这是一个“怎么样?” 情况。 不幸的是,如果这种情况发生,但我认为“使用”只是在那里有礼貌,而不是改变极其重要的程序状态。 我假设如果某个可怕的线程中止exception发生在错误的时间,那么垃圾收集器将最终通过运行终结器来清理该资源。

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

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

.SafeExecute(Action fribbleAction)方法包装trycatchfinally块。

在C#语言devise团队的Eric Gunnerson对这个问题给出了几乎相同的问题:

Doug问道:

重新:一个locking语句超时…

我之前已经完成了这个技巧来处理大量方法中的常见模式 。 通常locking收购 ,但也有一些其他的 。 问题是它总是感觉像一个黑客,因为对象不是真正的一次性“ callback到一个范围的能力 ”。

道格,

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

这是一个滑坡。 IDisposable有一个合同,由终结者备份。 终结者在你的情况下是无用的。 你不能强迫客户使用使用声明,只能鼓励他这样做。 你可以用这样的方法强制它:

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

但是如果没有拉姆达,这往往会变得有些尴尬。 也就是说,我已经像你一样使用了IDisposable。

然而,在你的文章中有一个细节,这使得这个危险地接近反模式。 你提到那些方法可以抛出一个exception。 这不是调用者可以忽略的。 他可以做三件事:

  • 什么都不做,exception是不可收回的。 正常的情况。 调用解锁并不重要。
  • 抓住并处理exception
  • 在他的代码中恢复状态,让exception通过呼叫链。

后两者要求调用者明确写一个try块。 现在使用声明阻碍了。 这可能会诱使一个客户昏迷,使他相信你的class级正在照顾国家,不需要额外的工作。 这几乎是不准确的。

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

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

要么

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

Html.EndForm调用Dispose,Dispose只输出</form>标签。 关于这一点的好处是{}括号创build了一个可见的“范围”,这使得更容易看到表单内的内容以及内容。

我不会过度使用它,但基本上IDisposable只是一种说法,“当你完成这个任务时你必须调用这个函数”。 MvcForm使用它来确保表单已closures,Stream使用它来确保该stream已closures,您可以使用它来确保对象已解锁。

我个人只有在以下两条规则是真的时才会使用它,但是这是由我任意设定的:

  • Dispose应该是一个总是必须运行的函数,所以除了NULL检查之外,不应该有任何条件
  • Dispose()之后,该对象不应该是可重用的。 如果我想要一个可重用的对象,我宁愿给它打开/closures的方法,而不是处置。 所以当我尝试使用一个处理对象时,我抛出了一个InvalidOperationExceptionexception。

最后,这完全是关于期望。 如果一个对象实现了IDisposable,我认为它需要做一些清理,所以我称之为。 我认为它通常打败了“关机”function。

这就是说,我不喜欢这一行:

 this.Frobble.Fiddle(); 

由于FrobbleJanitor现在“拥有”Frobble,我不知道在看门人的Frobble上叫Fidble是不是更好?

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

在这方面指出:我同意这里的最多,这是脆弱的,但有用的。 我想指出你System.Transaction.TransactionScope类,就像你想做的事情。

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

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

我理解IDisposable接口的方式是,一旦已经处理了对象,就不应该使用它(除非允许您随意调用Dispose方法)。

由于每次您using语句时FrobbleJanitor显式创build一个新的 FrobbleJanitor ,因此您从不使用同一个FrobbeJanitor对象两次。 而且因为它的目的是pipe理另一个对象,所以Dispose似乎适合释放这个(“pipe理”)资源的任务。

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

我唯一担心的是using (var janitor = new FrobbleJanitor())比使用LockUnlock操作直接可见的更明确的try..finally块更不明显。 但是采取哪种方法可能归结为个人喜好的问题。

注:我的观点可能偏离我的C ++背景,所以我的答案的价值应该评估,以反对可能的偏见…

什么说的C#语言规范?

引用C#语言规范 :

8.13使用声明

[…]

资源是实现System.IDisposable的类或结构,其中包含一个名为Dispose的单参数方法。 使用资源的代码可以调用Dispose来指示不再需要该资源。 如果Dispose未被调用,则自动处理最终会由于垃圾收集而发生。

使用资源的代码当然是using关键字开始的代码,直到使用的范围。

所以我猜这是正确的,因为锁是一个资源。

也许关键字using被严重select。 也许它应该被称为scoped

那么,我们几乎可以认为任何东西都是资源。 一个文件句柄。 networking连接…线程?

一个线程?

使用(或滥用) 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#编写的代码的C ++版本包含在析构函数中使用IDispose模式。 通过查看C ++版本,我们可以看到一个析构函数在退出之前等待内部线程结束,如文章的其他部分所示:

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

所以,就Herb而言,它是shiny的

它不是虐待。 您正在使用它们创build的内容。 但是,您可能需要根据自己的需要来考虑。 例如,如果你select了“艺术性”,那么你可以使用“使用”,但是如果你的代码块执行了很多次,那么出于性能的原因,你可以使用“试试”,“最终”的结构。 因为“使用”通常涉及对象的创作。

我想你是对的。 重载Dispose()将是一个问题,同一个类后来清理它实际上必须做的,并且那个清理的生命周期改变了不同,那么当你期望持有一个锁。 但是既然你创build了一个单独的类(FrobbleJanitor),只负责locking和解锁Frobble,事情已经解耦了,你不会碰到这个问题。

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