处理嵌套“using”语句时“Dispose”抛出的exception
显然,使用嵌套的using
语句时,一些exception可能会丢失。 考虑这个简单的控制台应用
using System; namespace ConsoleApplication { public class Throwing: IDisposable { int n; public Throwing(int n) { this.n = n; } public void Dispose() { var e = new ApplicationException(String.Format("Throwing({0})", this.n)); Console.WriteLine("Throw: {0}", e.Message); throw e; } } class Program { static void DoWork() { // ... using (var a = new Throwing(1)) { // ... using (var b = new Throwing(2)) { // ... using (var c = new Throwing(3)) { // ... } } } } static void Main(string[] args) { AppDomain.CurrentDomain.UnhandledException += (sender, e) => { // this doesn't get called Console.WriteLine("UnhandledException:", e.ExceptionObject.ToString()); }; try { DoWork(); } catch (Exception e) { // this handles Throwing(1) only Console.WriteLine("Handle: {0}", e.Message); } Console.ReadLine(); } } }
Throwing
每个实例在被处理时抛出。 永远不会调用AppDomain.CurrentDomain.UnhandledException
。
输出:
投掷:投掷(3) 投掷:投掷(2) 投掷:投掷(1) 处理:投掷(1)
我更喜欢至少能够记录丢失的Throwing(2)
和Throwing(3)
。 我如何做到这一点,而不是为每个using
单独的try/catch
(这将有点杀死using
的方便)?
在现实生活中,这些对象通常是我无法控制的类的实例。 他们可能会或可能不会投掷,但如果他们这样做,我希望可以选择观察这些例外情况。
当我在考虑降低嵌套using
的级别时出现了这个问题。 有一个简洁的答案暗示汇总exception。 有趣的是,这与嵌套的using
语句的标准行为有何不同。
[编辑]这个问题似乎密切相关: 你应该实现IDisposable.Dispose(),以便它永远不会抛出?
您注意到的是Dispose
和using
设计中的一个基本问题,目前还没有很好的解决方案。 恕我直言,最好的设计是拥有一个Dispose
版本,它接收任何可能挂起的exception(如果没有挂起,则为null
),如果需要抛出一个exception,可以记录或封装该exception。 否则,如果您既可以控制在using
中也可以在Dispose
导致exception的代码,那么您可以使用某种外部数据通道让Dispose
知道内部exception,但这是相当的做作。
太糟糕了,没有适当的语言支持与finally
块相关联的代码(显式地或隐式地通过using
)来知道相关的try
是否正确完成,如果没有,则出现了什么问题。 Dispose
应该默默地失败的概念是恕我直言,非常危险和错误的。 如果一个对象封装了一个打开写入的文件,并且Dispose
关闭了该文件(一个常见的模式)并且无法写入数据,那么让Dispose
调用正常返回会导致调用代码认为数据写得正确,可能允许它覆盖了唯一的好备份。 此外,如果文件应该显式关闭并且调用Dispose
而不关闭文件应该被视为错误,这意味着如果受保护的块将正常完成,但是如果受保护的块无法调用Close
,则Dispose
应该抛出exception因为首先发生exception,让Dispose
抛出exception将是非常无益的。
如果性能不是很关键,你可以在VB.NET中编写一个包装器方法,它接受两个代理(类型为Action
和一个Action
),在try
块中调用第一个,然后在finally
调用第二个块与try
块中发生的exception(如果有)。 如果包装器方法是用VB.NET编写的,它可以发现并报告发生的exception, 而不必捕获并重新抛出它。 其他模式也是可能的。 包装器的大多数用法都涉及闭包,这些闭包很笨,但包装器至少可以实现正确的语义。
另一种可以避免闭包的包装设计,但要求客户正确使用它并且几乎不提供防止错误使用的保护,这样会产生如下用法:
var dispRes = new DisposeResult(); ... try { .. the following could be in some nested routine which took dispRes as a parameter using (dispWrap = new DisposeWrap(dispRes, ... other disposable resources) { ... } } catch (...) { } finally { } if (dispRes.Exception != null) ... handle cleanup failures here
这种方法的问题在于没有办法确保任何人都会评估dispRes.Exception
。 可以使用终结器来记录在没有检查过的情况下放弃dispRes
情况,但是没有办法区分发生这种情况的情况,因为exception将代码踢出if
测试,或者因为程序员忘记了检查。
PS – Dispose
确实应该知道是否发生exception的另一种情况是,当IDisposable
对象用于包装锁或其他范围时,对象的不变量可能会暂时失效,但在代码离开范围之前需要恢复。 如果发生exception,代码通常不会期望解决exception,但是应该根据它进行操作 ,使锁既不保持也不释放,而是无效 ,以便任何现在或将来尝试获取它将引发exception。 如果将来没有尝试获取锁或其他资源,则它无效的事实不应该破坏系统操作。 如果资源对于程序的某些部分是非常必要的,那么使其无效将导致程序的该部分死亡,同时最小化它对其他任何事物造成的损害。 我知道用唯一的语义真正实现这个案例的唯一方法是使用icky闭包。 否则,唯一的选择是要求显式的invalidate / validate调用,并希望在资源无效的代码部分内的任何return语句之前都要调用validate。
有一个代码分析器警告。 CA1065 ,“不要在意外位置引发exception”。 Dispose()方法在该列表上。 “框架设计指南”第9.4.1章中的强烈警告:
避免在Dispose(bool)中抛出exception,除非在包含进程已被破坏的临界情况下(泄漏,不一致的共享状态等)。
这是错误的,因为using语句在finally块中调用Dispose()。 finally块中引发的exception可能会产生令人不快的副作用,如果在堆栈因exception而展开时调用finally块,则它会替换活动exception。 你到底发生了什么。
Repro代码:
class Program { static void Main(string[] args) { try { try { throw new Exception("You won't see this"); } finally { throw new Exception("You'll see this"); } } catch (Exception ex) { Console.WriteLine(ex.Message); } Console.ReadLine(); } }
也许一些辅助函数可以让你编写类似于using
代码:
void UsingAndLog(Func creator, Action action) where T:IDisposabe { T item = creator(); try { action(item); } finally { try { item.Dispose();} catch(Exception ex) { // Log/pick which one to throw. } } } UsingAndLog(() => new FileStream(...), item => { //code that you'd write inside using item.Write(...); });
请注意,我可能不会使用此路由,只是让Dispose
中的exception从正常using
代码中覆盖我的exception。 如果库从Dispose
反对强烈建议不这样做,那么很有可能它不是唯一的问题,并且需要重新考虑这种库的有用性。