是否滥用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
,因为我想要使用FrobbleJanitor
来using
。 我可以认为这是一个代码审查的问题,但有些事情是唠叨我。
问题:以上是否被认为是滥用和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)
方法包装try
– catch
– finally
块。
在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())
比使用Lock
和Unlock
操作直接可见的更明确的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。