"using"结构和异常处理

13
"using" 构造在需要同时处理开始和结束部分的情况下非常方便。
快速示例以说明:
using (new Tag("body")) {
    Trace.WriteLine("hello!");
}
// ...
class Tag : IDisposable {
    String name;
    public Tag(String name) {
        this.name = name;
        Trace.WriteLine("<" + this.name + ">");
        Trace.Indent();
    }
    public void Dispose() {
        Trace.Unindent();
        Trace.WriteLine("</" + this.name + ">")
    }
}

构造函数定义了起始部分,Dispose 方法定义了结束部分。

然而,尽管看起来很有吸引力,但这种结构存在一个严重的缺点,即 Dispose 方法是在 finally 块内调用的。因此,存在两个问题:

  1. 应避免在 finally 块中抛出异常,因为它们会覆盖本应被捕获的原始异常。

  2. 无法在 Dispose 方法中知道在“开始”和“结束”之间是否已经抛出异常,因此无法相应地处理“结束”部分。

这两个问题使得使用这种结构不切实际,这是一个非常令人沮丧的事实。

现在,我的问题是:

  1. 我对这些问题的理解正确吗?这就是“using”实际上的工作方式吗?

  2. 如果是这样,除了释放资源和清理外,“using”结构是否有其他方法可以克服这些问题并发挥实际作用?

  3. 如果“using”无法以这种方式实际使用,那么替代方法是什么(强制执行具有起始和结束部分的代码上下文)?


请查看此处:http://stackoverflow.com/questions/8865920/disposable-context-object-pattern 以获取更多相关信息。 - Eric Dahlvang
5个回答

5
您的规则#1无论是否使用都适用,因此规则#2是真正的决定因素:如果必须区分抛出异常和正常程序完成的情况,请选择try/catch。例如,如果持久层在使用数据库连接过程中发现问题,则需要关闭连接,无论是否出现异常。在这种情况下,using结构是一个完美的选择。在某些情况下,您可以设置using以特定方式检测正常完成与异常完成。环境事务提供了一个完美的例子:
using(TransactionScope scope = new TransactionScope()) {
    // Do something that may throw an exception
    scope.Complete();
}

如果在调用Complete之前,调用了scopeDisposeTransactionScope会知道发生了异常,并中止事务。

这并不是一个很好的例子,因为需要显式调用scope.Complete,这使得“using”的整个概念变得无用。你必须记住在每个地方都这样做,这与在代码周围普通地调用scope.Begin()和scope.End()没有太大区别。 - Trident D'Gao
但这正是“TransactionScope”的设计理念!无论导致不调用Complete的路径是什么,scope都知道出了问题(顺便说一下,“出了问题”包括“在编码阶段忘记调用Complete”)。而且,作用域存在的目的就是检测出现问题的情况:您对Complete的调用是您投票表示此事务部分进行得很好。 - Sergey Kalinichenko
1
@YavgenyP 如果您的代码检测到不应提交事务的情况,则允许在不调用Complete的情况下离开using块。 using块不比if语句“半成品”更多 :) 它是语言的完全定义特性。当正确使用时,它可以帮助您大大减少混乱。 - Sergey Kalinichenko
@bonomo,这个功能不能满足你的期望与该功能本身无关,而是与你的期望有关。忘记调用“Complete”不比忘记在行末加上分号是C#编译器的“缺陷” :) - Sergey Kalinichenko
1
@dasblinkenlight,使用“to”本来就是一种语法糖,它应该通过让我少打字并且不给我忘记执行“end”部分的机会来使我的生活更轻松。但这里并非如此,因此它仍然是语法糖,但带有一点苦涩的味道。 - Trident D'Gao
显示剩余2条评论

2
using语句和IDisposable接口的目的是为了让用户释放非托管资源。这些资源通常很昂贵而且珍贵,所以无论如何都必须进行处理(这就是为什么要使用finally)。finally块中的代码甚至不能被中止,否则会导致整个应用程序域关闭。

现在,很容易滥用using来完成你正在描述的任务,我以前也这样做过。大多数情况下没有什么危险性。但是如果发生意外异常,整个处理的状态都会受到损害,您不一定想运行结束操作;因此,总的来说,请不要这样做。

一种替代方法是使用lambda表达式,就像这样:

public interface IScopable { 
  void EndScope();
}

public class Tag : IScopable {
  private string name;
  public Tag(string name) {
    this.name = name;
    Trace.WriteLine("<" + this.name + ">");
    Trace.Indent();
  }
  public void EndScope() {
    Trace.Unindent();
    Trace.WriteLine("</" + this.name + ">");
  }
}

public static class Scoping {
  public static void Scope<T>(this T scopable, Action<T> action) 
    where T : IScopable {
    action(scopable);
    scopable.EndScope();
  }
}

像这样使用:

new Tag("body").Scope(_ => 
  Trace.WriteLine("hello!")
);

您还可以创建其他实现,根据是否引发异常来运行某些操作。

在Nemerle语言中,可以通过扩展新的语法来支持此功能。


1
是的,似乎使用包装的匿名函数是唯一可行的替代方案,尽管我不喜欢这个想法,因为它看起来不够优雅,而且会带来使用闭包的所有注意事项。 - Trident D'Gao
1
我最终使用了lambda表达式。虽然不是最高效的方法,但非常直观并且能够恰到好处地完成任务。所以你的答案获胜了。 - Trident D'Gao

2
我不知道这是否是IDisposable的最初意图,但Microsoft确实正在使用您描述的方式(分离开始和结束部分)。一个很好的例子是由mvc基础结构提供的MVCForm类。它实现了IDisposable并编写了表单的结束标记,而我看不到它的实现会释放任何资源(在那里使用的writer似乎甚至在表单被处理后仍然存在)。
关于using块以及它如何“吞噬”异常已经写了很多(wcf client是一个很好的示例,在SO上也可以找到这样的讨论)。个人感觉,尽管使用using块很方便,但何时应该使用它,何时不应该使用它并不完全清楚。
当然,实际上您可以在处理方法中告诉是否有错误,方法是 向您的类添加额外标志,并在 using 块中引发它,但前提是使用您的类的人需要知道该标志。

1

您在观察try/finally块设计中存在的问题时是正确的,这也是using存在的问题:在finally块中的代码无法干净地知道代码执行是否将继续进行以下finally块后面的语句,或者是否存在一个挂起的异常,该异常将在finally块执行后立即有效地接管。

我真的很希望看到vb.net和C#中的语言功能,允许finally块包括一个Exception参数(例如:

  try
  {
  }
  finally (Exception ex)
  {
    ...
  }

如果try块正常退出,则传入的异常将为null,否则将保存异常。除此之外,我想看到一个IDisposableEx接口,它将继承Dispose,并包括一个Dispose(Exception ex)方法,期望用户代码从finally块中传递ex。在Dispose期间发生的任何异常都可以包装传入的异常(因为传入的异常和在Dispose中发生异常的事实都是相关的)。

如果无法实现上述功能,.net提供一种指示当前上下文中是否有待处理异常的方法可能会有所帮助。不幸的是,在各种边缘情况下,这种方法的确切语义并不清楚。相比之下,finally (Exception ex)的语义将非常清晰。顺便说一句,正确实现finally (Exception ex)需要语言使用异常过滤器,但不需要公开创建任意过滤器的能力。


finally 子句中的代码 不应该关注 是否抛出异常。它应该能够并且应该无论如何做完全相同的事情。如果你只想在出现异常时执行某些操作,那么这就是 catch 子句该做的事情。 - cHao
@cHao:catch块应该表明“好的,我知道如何将一切恢复正常”。虽然可以使用throw;来表示“...嗯,也许我不知道”,但是使用带有无条件重新抛出的catch是对catch的滥用;让finally知道catch退出的方式相比之下要小得多。实际上,框架允许定义fault块,在这里可能是最合适的;它们仅在抛出异常时执行,但它们保留异常挂起。不幸的是,C#和vb.net都不支持它们。虽然VB.net可以模拟它们,但C#则不能。 - supercat
1
我认为没有真正的理由不允许它们获取锁。锁的工作不是保证不会出现损坏,而是锁定。确保自己具有独占访问权。不多也不少。如果资源无法确保自身的完整性,则需要重新设计 - 或者至少将其包装在可以纠正此类问题或发出信号的东西中。 - cHao
1
@cHao:包括Eric Lippert在内的许多有见识的人都认为,在释放一个处于未知状态的实体上的锁定是危险的,最好让潜在的消费者被阻塞,而不是让他们访问一个已经损坏的实体。如果这是真的,我认为最好让潜在的消费者发现资源已经失效,而不是永远等待永远不会可用的东西。 - supercat
1
这个东西确实处于已知状态。“损坏”。:P 它无法检测到并相应地做出反应,这是其设计上的根本缺陷,应该在那里修复。而不是通过将一些垃圾附加到异常处理程序上来解决问题,这只会说“如果发生此异常,请执行此操作”。特别是当catch已经有了这个功能时。 - cHao
显示剩余7条评论

0
在我看来,您误用了 IDisposable 接口。 通常的做法是使用该接口来释放非托管资源。通常情况下,垃圾回收器会清理对象,但在某些情况下 - 当您不再需要该对象时 - 您可能需要手动清理。
然而,在您的情况下,您并没有清理不再需要的对象;您正在使用它来强制执行某些逻辑。您应该使用另一种设计来解决这个问题。

3
“using”类似于OP所做的操作是一种相当常见的模式。 - CodesInChaos
2
IDisposable 的目的是允许类说:“我知道有些事情需要在现在和宇宙末日之间完成;我有信息和动力去做它,很可能没有其他人这样做。请确保在适当的时候调用我的 IDisposable.Dispose 方法,并在放弃使用我之前调用它。谢谢。” 我觉得这里提供的用法完全符合这种模式。 - supercat
2
@supercat:抱歉,不行。IDisposable的目的在接口本身的文档中已经明确说明:“定义释放分配资源的方法”。即使是微软自己这样做,超出该范围的使用也是真正的误用。 - cHao
1
@supercat:我想“未受管理的资源”不够吧..? :) 基本上,任何CLR不跟踪或自动清理的东西。内存通常不计算在内(尽管对GlobalAlloc / HeapAlloc等的调用可能会让您获得未受管理的内存,如果您感到痛苦),但GDI / kernel / window句柄是。 - cHao
1
@cHao:这将引出“什么是资源”的问题?我认为,任何实体在宇宙中的任何地方,以任何方式改变其行为以代表一个对象,对其他实体造成损害,并将继续这样做直到另行通知,都可以视为资源。与IDisposable相关的资源是,除非采取某些特定的行动将其从该状态中移出,否则会使某些东西处于糟糕的状态,而实现IDisposable的对象知道该状态是什么。就原始问题而言,文件处于“糟糕”状态... - supercat
显示剩余16条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接