113

我在 C++ 中经常使用的东西是让一个类通过构造函数和析构函数A处理另一个类的状态进入和退出条件,以确保如果该范围内的某些东西抛出异常,那么 B 在范围已退出。就首字母缩略词而言,这并不是纯粹的 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

问题:以上是否会被视为滥用usingand IDisposable

4

12 回答 12

72

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

出于三个原因,我认为这是一种滥用行为。

首先,因为我希望“使用”用于使用资源在您使用完资源后将其丢弃。更改程序状态不使用资源,将其更改回不处理任何内容。因此,“使用”来改变和恢复状态是一种滥用;该代码会误导普通读者。

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

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

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

也许是,也许不是。我的观点是,对于最初编写的代码,代码审查者知道要问这个问题。使用“使用”的代码,我不知道要问这个问题;我假设“使用”块分配了一个资源,使用它一点,并在完成后礼貌地处理它,而不是“使用”块的右括号在任意多的特殊情况下改变我的程序状态违反了程序状态一致性条件。

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

}

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

如果我看到您的原始代码,我会问的第二件事是“如果在 Unlock 之后但在输入 try 之前引发异常会发生什么?” 如果您正在运行未优化的程序集,编译器可能在尝试之前插入了无操作指令,并且可能在无操作上发生线程中止异常。这种情况很少见,但在现实生活中确实会发生,尤其是在 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();
}

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

于 2010-01-20T16:45:52.397 回答
35

我不这么认为,必然。从技术上讲, IDisposable旨在用于具有非托管资源的事物,但是 using 指令只是实现try .. finally { dispose }.

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

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

有很多其他的选择可以做到这一点,但最终,我想不出任何能像这样整洁的方法,所以去吧!

于 2010-01-20T13:22:25.010 回答
27

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

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

.SafeExecute(Action fribbleAction)方法包装了try--块catchfinally

于 2010-01-20T13:31:59.440 回答
27

C# 语言设计团队的 Eric Gunnerson对几乎相同的问题给出了这个答案:

道格问:

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

我以前做过这个技巧来处理许多方法中的常见模式。通常是锁获取,但也有一些其他的。问题是它总是感觉像是一种黑客攻击,因为该对象并不是真正可丢弃的,而是“可以在范围结束时回调”。

道格,

当我们决定 [原文如此] using 语句时,我们决定将其命名为“使用”,而不是更具体地用于处理对象,以便它可以用于这种场景。

于 2010-01-20T17:00:37.070 回答
11

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

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

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

但是,您的帖子中有一个细节使这危险地接近于反模式。您提到这些方法可能会引发异常。这不是调用者可以忽略的。对此,他可以做三件事:

  • 什么都不做,异常是不可恢复的。正常情况。调用解锁无关紧要。
  • 捕获并处理异常
  • 在他的代码中恢复状态并让异常通过调用链。

后两者要求调用者显式编写一个 try 块。现在 using 语句妨碍了。这很可能使客户陷入昏迷,使他相信您的班级正在照顾状态,不需要做额外的工作。这几乎从来都不是准确的。

于 2010-01-20T13:45:49.730 回答
8

一个真实的例子是 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 会不会更好?

于 2010-01-20T20:13:15.493 回答
4

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

C# 语言规范怎么说?

引用C# 语言规范

8.13 using 语句

[...]

资源是实现 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# 对 C++ 版本包含在析构函数中的代码使用 IDispose 模式。通过查看 C++ 版本,我们看到了一个析构函数,它在退出之前等待内部线程结束,如本文的其他摘录所示:

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

所以,就 Herb 而言,它是闪亮的。

于 2010-08-27T13:26:11.443 回答
4

对此表示赞同:我同意这里的大多数人的观点,即这是脆弱的,但很有用。我想向您指出System.Transaction.TransactionScope类,它执行您想做的事情。

一般来说,我喜欢这种语法,它消除了很多杂乱无章的东西。请考虑给辅助类起一个好听的名字——也许...Scope,就像上面的例子一样。该名称应该表明它封装了一段代码。*Scope、*Block 或类似的东西应该这样做。

于 2010-01-20T13:53:38.293 回答
4

我们在我们的代码库中大量使用了这种模式,而且我以前到处都看到过——我相信这里也一定已经讨论过了。一般来说,我看不出这样做有什么问题,它提供了一种有用的模式并且不会造成真正的伤害。

于 2010-01-20T13:22:01.410 回答
3

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

我理解IDisposable接口的方式是,一旦对象被释放,你就不应该使用它(除了你可以根据需要Dispose经常调用它的方法)。

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

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

我个人唯一担心的是,与直接可见&操作using (var janitor = new FrobbleJanitor())的更明确try..finally的块相比,发生了什么不太清楚。但是采取哪种方法可能归结为个人喜好问题。LockUnlock

于 2010-01-20T13:30:47.160 回答
1

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

于 2010-01-20T13:54:45.753 回答
1

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

不过,我会将 FrobbleJanitor 重命名为类似 FrobbleLockSession 之类的名称。

于 2010-01-20T19:07:51.740 回答