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

Joh*_*ell 107 c# exception-handling raii

我经常在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();
    }
}
Run Code Online (Sandbox Code Playgroud)

这需要像这样做

private void FiddleTheFrobble()
{
    this.Frobble.Unlock();

    try
    {            
        Foo();                  // Can throw
        this.Frobble.Fiddle();  // Can throw
        Bar();                  // Can throw
    }
    finally
    {
        this.Frobble.Lock();
    }
}
Run Code Online (Sandbox Code Playgroud)

如果我想保证退货Frobble时的状态FiddleTheFrobble.代码会更好

private void FiddleTheFrobble()
{
    using (var janitor = new FrobbleJanitor(this.Frobble))
    {            
        Foo();                  // Can throw
        this.Frobble.Fiddle();  // Can throw
        Bar();                  // Can throw
    }
}
Run Code Online (Sandbox Code Playgroud)

这里FrobbleJanitor看起来大致

class FrobbleJanitor : IDisposable
{
    private Frobble frobble;

    public FrobbleJanitor(Frobble frobble)
    {
        this.frobble = frobble;
        this.frobble.Unlock();
    }

    public void Dispose()
    {
        this.frobble.Lock();
    }
}
Run Code Online (Sandbox Code Playgroud)

这就是我想要的方式.现在现实赶上,因为我使用要求FrobbleJanitor使用 using.我可以认为这是一个代码审查问题,但有些事情在唠叨我.

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

Eri*_*ert 69

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

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

首先,因为我希望"使用"用于使用资源在完成后处理它.更改程序状态不使用资源,更改它不会丢弃任何内容.因此,"使用"变异和恢复状态是一种滥用; 这段代码误导了随意的读者.

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

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

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

也许是,也许不是.我的观点是,使用最初编写的代码,代码审阅者知道提出问题.使用"使用"的代码,我不知道问这个问题; 我假设"using"块分配一个资源,稍微使用它,并在完成时礼貌地处理它,而不是"using"块的括号在异常情况下在异常情况下改变我的程序状态程序状态一致性条件已被违反.

使用"using"块具有语义效果使得该程序片段:

}
Run Code Online (Sandbox Code Playgroud)

非常有意义.当我看到那个单一的紧密支撑时,我不会立即认为"该支具具有副作用,这对我的程序的全局状态具有深远的影响".但是,当你像这样滥用"使用"时,它会突然发生.

如果我看到你的原始代码,我要问的第二件事是"如果在解锁之后但在输入尝试之前抛出异常会发生什么?" 如果您正在运行非优化程序集,则编译器可能在尝试之前插入了无操作指令,并且可能会在无操作上发生线程中止异常.这种情况很少见,但它确实发生在现实生活中,特别是在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();
}
Run Code Online (Sandbox Code Playgroud)

也许它确实如此,也许它没有,但我知道问这个问题.对于"使用"版本,它容易出现同样的问题:在Frobble被锁定之后但在输入与使用相关联的尝试保护区域之前,可能抛出线程中止异常.但是使用"使用"版本,我认为这是"那么什么?" 情况.不幸的是,如果发生这种情况,但我认为"使用"只是礼貌,而不是改变极其重要的程序状态.我假设如果在错误的时间发生了一些可怕的线程中止异常,那么,好吧,垃圾收集器最终将通过运行终结器来清理该资源.

  • 虽然我会以不同的方式回答这个问题,但我认为这是一个有争议的帖子.但是,我对你声称"使用"是礼貌而不是正确性问题提出质疑.我认为资源泄漏是正确性问题,尽管它们通常很小,以至于它们不是高优先级的错误.因此,"使用"块*已经*对程序状态产生语义影响,它们阻止的不仅仅是对其他进程"不便",而且可能是一个破碎的环境.这并不是说问题所暗示的不同类型的语义影响也是恰当的. (18认同)
  • 资源泄漏是正确性问题,是的.但是没有使用"使用"不会泄漏资源; 终结器运行时,资源最终将被清除.清理可能不像你想的那样具有攻击性和及时性,但它会发生. (12认同)
  • 虽然我普遍同意你的立场,但在某些情况下,重新使用"使用"的好处太有吸引力而无法放弃.我能想到的最好的例子是跟踪和报告代码时序:`using(TimingTrace.Start("MyMethod")){/*code*/}`再次,这是AOP - `Start()`捕获开始时间在块开始之前,`Dispose()`捕获结束时间并记录活动.它不会更改程序状态,并且与异常无关.它也很有吸引力,因为它避免了相当多的管道,并且它经常被用作模式,减轻了令人困惑的语义. (11认同)
  • 精彩的帖子,这是我的两分钱:)我怀疑"使用"的"滥用"很多是因为它是一个穷人在C#中面向方面的编织者.开发人员真正想要的是一种方法来保证一些公共代码将在块的末尾运行而不必复制该代码.C#今天不支持AOP(MarshalByRefObject和PostSharp不能支持).但是,编译器对using语句的转换使得通过重新确定确定性资源处置的语义来实现简单的AOP成为可能.虽然不是一个好的做法,但它有时候很有吸引力..... (7认同)
  • 有趣的是,我一直认为使用它作为支持RAII的手段.对于我来说,将锁视为一种资源似乎是相当直观的. (6认同)
  • @Brian:锁可能是一种资源.互斥是一个操作系统句柄,这些是一个稀缺的全球资源.但是***应该*及时释放**语义上重要的突变**必须*在特定时间发生以维持*逻辑程序不变*之间存在很大差异. (4认同)
  • "我希望"使用"是出于礼貌而非必要而使用" - 导致Web应用程序在负载下耗尽DB连接池而未能确定性地关闭连接不仅仅是缺乏礼貌. (3认同)

And*_*tan 32

我不这么认为,必然.IDisposable的技术上指用于事物具有非托管资源,但随后的使用指令是实现的一个常见的模式只是一种巧妙的方法try .. finally { dispose }.

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

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

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

  • 我相信它也是"使用"的艺术用途.我认为Samual Jack的帖子链接到Eric Gunnerson的评论,证实了这种"使用"的实现.我可以看到安德拉斯和埃里克在这方面的优秀观点. (2认同)
  • @Haacked - 有趣的不是它; 你的评论完全证明了我的观点; 你曾经和团队中的一个人谈过,而他全都是为了这个; 然后在同一支球队不同意的情况下指出Eric Lippert.在我看来,我的观点是; C#提供了一个利用接口的关键字,并且恰好产生了一个漂亮的代码模式,可以在很多场景中运行.如果我们不应该滥用它,那么C#应该找到另一种方法来强制执行它.无论哪种方式,设计师可能需要在这里连续得到他们的鸭子!在此期间:`使用(Html.BeginForm())`先生?我不介意我先生!:) (2认同)

her*_*ter 27

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

myFribble.SafeExecute(() =>
    {
        myFribble.DangerDanger();
        myFribble.LiveOnTheEdge();
    });
Run Code Online (Sandbox Code Playgroud)

其中.SafeExecute(Action fribbleAction)方法包装try- catch- finally块.

  • 育.为什么要隐藏try/catch/finally?使您的代码隐藏以供其他人阅读. (4认同)
  • @Yukky Frank:“隐藏”任何东西不是我的主意,这是发问者的要求。:-P ...也就是说,请求最终只是“不要重复自己”的问题。您可能有很多方法都需要使用相同的样板“框架”来干净地获取/释放某些内容,而又不想将其强加给调用者(想想封装)。也可以更清晰地命名诸如.SafeExecute(...)之类的方法,以充分传达其功能。 (2认同)

Sam*_*ack 24

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

道格问道:

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

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

道格,

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

  • 那个引用应该说出他所说的"正是这种情况"时所指的是什么,因为我怀疑他是指这个未来的问题. (4认同)
  • @Frank Schwieterman:我完成了报价.显然,来自C#团队的人**确实认为"使用"功能**不仅仅限于资源处理. (3认同)

Han*_*ant 11

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

void UseMeUnlocked(Action callback) {
  Unlock();
  try {
    callback();
  }
  finally {
    Lock();
  }
}
Run Code Online (Sandbox Code Playgroud)

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

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

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

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


Mic*_*tum 7

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

Html.BeginForm(...);
Html.TextBox(...);
Html.EndForm();
Run Code Online (Sandbox Code Playgroud)

要么

using(Html.BeginForm(...)){
    Html.TextBox(...);
}
Run Code Online (Sandbox Code Playgroud)

Html.EndForm调用Dispose,Dispose只输出</form>标记.关于这一点的好处是{}括号创建了一个可见的"范围",这使得更容易看到表单中的内容和不是什么.

我不会过度使用它,但基本上IDisposable只是一种说"当你完成它时你必须调用这个功能"的方法.MvcForm使用它来确保表单关闭,Stream使用它来确保流关闭,您可以使用它来确保对象被解锁.

就个人而言,如果以下两条规则属实,我只会使用它,但我是由我设定的:

  • Dispose应该是一个始终必须运行的函数,因此除了Null-Checks之外不应该有任何条件
  • 在Dispose()之后,该对象不应该是可重用的.如果我想要一个可重用的对象,我宁愿给它打开/关闭方法而不是处理它.因此,在尝试使用已处置的对象时,我会抛出InvalidOperationException.

最后,这完全取决于期望.如果一个对象实现了IDisposable,我认为它需要做一些清理,所以我称之为.我认为它通常胜过"关机"功能.

话虽这么说,我不喜欢这句话:

this.Frobble.Fiddle();
Run Code Online (Sandbox Code Playgroud)

由于FrobbleJanitor现在"拥有"Frobble,我想知道在看门人的Frobble上打电话给Fiddle是不是更好?

  • 为提供来自 Microsoft 本身的示例而投票。请注意,在您提到的情况下会抛出一个特定的异常:ObjectDisposedException。 (2认同)