使用 IDisposable 和“ using”作为获得异常安全的“作用域行为”的手段是否是滥用的?

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

In C#, I often want to do

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使用的是 with using。我可以认为这是一个代码审查问题,但有些事情一直困扰着我。

问: 上述是否会被视为滥用 usingIDisposable

7085 次浏览

我们在代码库中大量使用了这种模式,我以前在各个地方都看到过这种模式——我确信这里也讨论过这种模式。一般来说,我不认为这样做有什么错,它提供了一个有用的模式,并没有造成真正的伤害。

我不这么认为。从技术上讲,IDisposable is意味着用于具有非托管资源的事物,但是 using 指令只是实现 try .. finally { dispose }通用模式的一种简单方法。

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

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

还有很多其他的方法可以做到这一点,但是,最终,我想不出任何一种方法会像这样简洁,所以去做吧!

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

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

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

(Btw., the standard sample code demonstrating the proper implementation of Dispose almost always suggests that managed resources should be freed, too, not just unmanaged resources such as file system handles.)

我个人担心的唯一一件事是,与使用更明确的 try..finally块(其中 LockUnlock操作可以直接看到)相比,使用 using (var janitor = new FrobbleJanitor())所发生的情况并不那么清楚。但是采取哪种方法可能取决于个人喜好。

If you just want some clean, scoped code, you might also use lambdas, á la

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

where the .SafeExecute(Action fribbleAction) method wraps the try - catch - finally block.

It is a slippery slope. IDisposable has a contract, one that's backed-up by a finalizer. A finalizer is useless in your case. You cannot force the client to use the using statement, only encourage him to do so. You can force it with a method like this:

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

但是没有 Lamdas 会有点尴尬,我也像你一样用过 IDisposable。

然而,在你的文章中有一个细节使得这个危险接近成为一个反模式。您提到过这些方法可以引发异常。打电话的人不能忽视这一点。对此,他可以做三件事:

  • Do nothing, the exception isn't recoverable. The normal case. Calling Unlock doesn't matter.
  • Catch and handle the exception
  • 在他的代码中恢复状态,并让异常通过调用链。

后两种方法要求调用方显式编写 try 块。现在 using 语句挡住了去路。这很可能会使客户陷入昏迷,使他相信你的班级正在照顾国家,没有额外的工作需要做。这几乎从来都不准确。

附和一下: 我同意这里的大多数人的看法,这是脆弱的,但是有用的。我想指给你们看 系统、事务、事务作用域类,它可以做一些你们想做的事情。

总的来说,我喜欢它的语法,它从实际的肉中去除了很多杂乱的东西。请考虑给助手类一个好的名字-也许... 范围,像上面的例子。名称应该暴露出它封装了一段代码。* Scope,* Block 或者类似的东西应该可以做到。

这不是虐待。你正在使用它们,它们就是为此而创造的。但是你可能需要根据你的需要考虑一个接一个。例如,如果您选择“ artistry”,那么您可以使用“ using”,但是如果您的代码片段执行了很多次,那么出于性能原因,您可以使用“ try”。.“ finally”结构,因为“ using”通常涉及到对象的创建。

我认为这是滥用使用陈述。我知道在这个问题上我是少数派。

我认为这是一种虐待,原因有三。

首先,因为我期望“ using”用于 利用资源用完了就把它处理掉。改变程序状态不是 使用资源,改变它回来不是 处置任何东西。因此,“使用”变异和恢复状态是一种滥用; 代码会误导一般的读者。

其次,因为我希望使用“ using”作为 出于礼貌,而非必要。在处理完一个文件后使用“ using”来处理它的原因不是因为它是 有需要,而是因为它是 有礼貌——其他人可能正在等待使用该文件,所以说“ done now”是道德上正确的做法。我希望我能够重构一个“ using”,这样使用过的资源可以保留更长的时间,并在以后处理掉,这样做的唯一影响就是 对其他程序造成轻微不便。具有 语义对程序状态的影响的“ using”块是滥用的,因为它在一个结构中隐藏了一个重要的、必需的程序状态变异,看起来它是为了方便和礼貌而存在的,而不是为了必要性。

And third, your program's actions are determined by its state; the need for careful manipulation of state is precisely why we're having this conversation in the first place. Let's consider how we might analyze your original program.

如果您将这个问题带到我的办公室进行代码审查,我会问的第一个问题是“如果抛出异常,那么锁定这个错误是否正确?”从您的程序中可以很明显地看出,无论发生什么情况,这个程序都会积极地重新锁定错误。抛出了异常。程序处于未知状态。我们不知道 Foo,Fiddle 或 Bar 是否投掷,他们为什么投掷,或者他们对其他没有清除的状态执行了什么变异。你能告诉我,在那种糟糕的情况下,重新锁定 一直都是是正确的做法吗?

Maybe it is, maybe it isn't. My point is, that with the code as it was originally written, 代码审查人员知道如何提出问题. With the code that uses "using", I don't know to ask the question; I assume that the "using" block allocates a resource, uses it for a bit, and politely disposes of it when it is done, not that the 闭合花括号 of the "using" block 当任意多个程序状态一致性条件被违反时,在异常情况下改变我的程序状态。

使用“ using”块产生语义效果使得这个程序片段:

}

非常有意义。当我看到那个单独的括号时,我不会马上想到“那个括号有副作用,它会对我的程序的全局状态产生深远的影响”。但是当你像这样滥用“使用”的时候,突然之间它就变了。

如果我看到您的原始代码,我会问的第二件事是“如果在解锁之后但在尝试输入之前抛出异常,会发生什么情况?”如果您正在运行一个未经优化的程序集,编译器可能在尝试之前插入了一条 no-op 指令,并且在 no-op 上有可能发生线程中止异常。这种情况很少见,但在现实生活中确实会发生,尤其是在网络服务器上。在这种情况下,解除锁定会发生,但锁定永远不会发生,因为异常是在尝试之前抛出的。完全有可能这段代码容易受到这个问题的影响,并且应该被编写出来

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();
}

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

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

道格问道:

Re: 带超时的锁语句..。

我以前用过这种方法来处理 共同的模式。通常是 锁定装置锁定装置但也有 其他人。问题是,它总是感觉像一个黑客,因为对象不是真正的一次性,因为这样的“ 在范围结束时回调”。

道格,

当我们决定使用 using 语句时,我们决定将其命名为“ using”,而不是更具体地处理对象,这样它就可以准确地用于这个场景。

我觉得你做得对。重载 Dispose ()将是同一个类后来必须执行的清理工作的一个问题,并且清理工作的生命周期与您希望持有锁的时间不同。但是,由于您创建了一个单独的类(FrobbleJanitor) ,它只负责锁定和解锁 Frobble,因此解耦程度足以让您无法解决这个问题。

不过我会把它改名为 FrobbleJanitor,可能是 FrobbleLockSession 之类的名字。

一个真实的例子是 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 ()之后,对象不应该是可重用的。如果我想要一个可重用的对象,我宁愿给它一个 open/close 方法,而不是释放。因此,当尝试使用已释放的对象时,将引发 InvalidOperationException。

At the end, it's all about expectations. If an object implements IDisposable, I assume it needs to do some cleanup, so I call it. I think it usually beats having a "Shutdown" function.

That being said, I don't like this line:

this.Frobble.Fiddle();

既然 FrobbleJanitor 现在“拥有”了 Frobble,我不知道是否应该把 Fiddle 叫做“清洁工中的 Frobble”?

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

What Says the C# Language Specification?

Quoting C # 语言规范:

8.13 The using statement

[...]

资源是实现 System.IDisposable 的类或结构,它包含一个名为 Dispose 的无参数方法。正在使用资源的代码可以调用 Dispose 来指示不再需要该资源。如果没有调用 Dispose,那么最终将由于垃圾收集而发生自动处理。

当然,使用资源的代码是从 using关键字开始直到附加到 using的作用域的代码。

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

Perhaps the keyword using was badly chosen. Perhaps it should have been called scoped.

然后,我们可以考虑几乎任何东西作为资源。一个文件句柄。一个网络连接... 一个线程?

一根线?

Using (or abusing) the using keyword?

闪闪发光(ab)使用 using关键字来确保线程的工作在退出作用域之前结束吗?

Herb Sutter 似乎认为这是 闪闪发光,因为他提供了 IDispose 模式的一个有趣用法,可以等待线程的工作结束:

Http://www.drdobbs.com/go-parallel/article/showarticle.jhtml?articleid=225700095

Here is the code, copy-pasted from the article:

// 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

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

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

所以,对赫伯来说,就是 shiny