使用 ExceptionDispatchInfo 捕捉并重新抛出异常

使用 ExceptionDispatchInfo 捕捉并重新抛出异常

发布于 2017-10-23 14:22 更新于 2017-10-26 23:38

当你跑起了一个异步线程,并用 await 异步等待时,有没有好奇为什么能够在主线程 catch 到异步线程的异常?

当你希望在代码中提前收集好异常,最后一并把收集到的异常抛出的时候,能不能做到就像在原始异常发生的地方抛出一样?

本文介绍 ExceptionDispatchInfo,专门用于重新抛出异常。它在 .NET Framework 4.5 中首次引入,并原生在 .NET Core 和 .NET Standard 中得到支持。


先探索为什么需要重新抛出异常,再了解如何最佳地重新抛出异常。如果你只希望了解 ExceptionDispatchInfo,请直接从以下导航中点击跳转到最后一节。

重新抛出异常

说起重新抛出异常,你是否会认为就是写出如下代码?

try
{
    DoButExceptionsMayOccur();
}
catch(Exception ex)
{
    // 在这里进行抢救。
    // 永远不要写出下面这句代码!(Don't write the code below forever!)
    throw ex;
}

为了防止这段代码被意外复制出去危及项目,我特地在注释中标明了永远不应该直接写出 throw ex 这样的句子!

这是因为 throw 语句会为异常的实例填充调用栈信息,范围为 throw 的地方开始,到 catch 的地方结束。也就是说,在异常刚刚发生的时候,也就是 DoButExceptionsMayOccur 里面的某一个调用会成为调用栈的起点,上面写了 catch 所在的函数会成为调用栈的终点。然而,一旦在 catch 中写出了 throw ex 这样的语句,那么 ex 中的调用栈将会被重写,范围从这一句 throw 开始,到外面能 catch 的地方为止。

具体说来,假设上面那段代码出现在 Test 方法中,里面的 DoButExceptionsMayOccur 调用了方法 InnerInner 中发生了异常;而 Outer 调用了 Test 方法,Outer 中也 catch 了异常;即整个调用链为 Outer->Test->DoButExceptionsMayOccur->Inner。那么,当刚刚 catch 到异常时,ex 的调用栈为 Test->DoButExceptionsMayOccur->Inner,而如果写了 throw ex,那么 Outer 中将只能发现调用栈为 Outer->Test,丢失了内部真正出错的原因,这对诊断和修复异常非常不利!

如果只是为了解决上述文字中所说的问题,其实只需要去掉那个 ex 即可,即:

try
{
    DoButExceptionsMayOccur();
}
catch(Exception)
{
    // 在这里进行抢救。
    throw;
}

然而,有时候这个异常并不直接从这里抛出(例如后台线程),或者说我们期望这是一个分步骤收集的异常(例如遍历)。这两种情况都有一个共同特点,就是重新抛出的地方根本就不在 catch 的地方。

后台线程的例子:

Exception exception = null;
DoSomething(() =>
{
    // 这个 try-catch 块将在另一个线程执行。
    try
    {
        DoButExceptionsMayOccur();
    }
    catch(Exception ex)
    {
        exception = ex;
    }
});
if (exception != null)
{
    // 重新抛出异常。
}

收集异常的例子:

List<Exception> exceptions = new List<Exception>();
foreach(var item in collection)
{
    try
    {
        DoButExceptionsMayOccur(item);
    }
    catch(Exception ex)
    {
        exceptions.Add(ex);
    }
}
if (exceptions.Any())
{
    // 重新抛出异常。
}

使用内部异常

.NET Framework 早期就提供了内部异常功能,专为解决保留调用栈而重新抛出异常而生。上面两段代码标记为// 重新抛出异常。的注释部分改为:

// 对应第一种情况。
throw new XxxException(ex);
// 对应第二种情况。
throw new AggregateException(exceptions);

于是两边的调用栈就被分别保留在了多个不同的 Exception 实例中。然而看异常总要一层层点开查看,始终不便。尤其是从产品中收集异常时,如何在异常分析系统中显示和分析也是个问题。

ExceptionDispatchInfo

如果将第一种情况写为:

ExceptionDispatchInfo.Capture(ex).Throw();

那么,这时外面的方法再 catch 异常,则会从外层直接看到里层,只在中间插入了一段文字,却看起来就像直接从原始出处抛出一样。

第二种情况写为:

if(exceptions.Count == 1)
{
    ExceptionDispatchInfo.Capture(exceptions.First()).Throw();
}
else if(exceptions.Count > 1)
{
    throw new AggregateException(exceptions);
}

使用这种方式,你看到的调用栈将是这样的:

至于多个异常的情况,那就只能使用内部异常来处理了。

而这些,正是 Task 管理异步线程异常时采用的策略——单个异常直接在调用线程直接抛出,多个异常抛出 AggregateException

本文会经常更新,请阅读原文: https://walterlv.com/post/exceptiondispatchinfo-capture-throw.html ,以避免陈旧错误知识的误导,同时有更好的阅读体验。

本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。欢迎转载、使用、重新发布,但务必保留文章署名 吕毅 (包含链接: https://walterlv.com ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。如有任何疑问,请 与我联系 (walter.lv@qq.com)

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏ml

C++ template的一些高级用法(元编码,可变参数,仿函数,using使用方法,. C++ 智能指针)

1 .  通用函数可变参数模板      对于有些时候,我们无法确切的知道,函数的参数个数时,而又不想过多的使用所谓的函数重载,那么就可以效仿下面的例子: 1...

88240
来自专栏前端进阶之路

JavaScript异步编程

#前言 从我们一开始学习JavaScript的时候就听到过一段话:JS是单线程的,天生异步,适合IO密集型,不适合CPU密集型。但是,多数JavaScript开...

11520
来自专栏每周一脱topic

Effictive python学习总结连载(1)

python从读研开始就在用了,拿来做过web后台、安全分析、爬虫、测试框架等等,挺强大的。最近借放假和看书和整理的机会,系统的总结下。主要是2方面:一个是书或...

22020
来自专栏進无尽的文章

设计模式| 行为型模式 (上)

行为型模式共十一种:策略模式、模板方法模式、观察者模式、迭代器模式、解释器模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式。 分两篇文...

17020
来自专栏Java帮帮-微信公众号-技术文章全总结

Java代码效率优化【面试+提高】

JAVA代码效率优化 最近在想自己编程时是否注意过代码的效率问题,得出的答案是:没有。代码只是实现了功能,至于效率高不高没怎么关注,这应该是JAVA程序员进阶...

470130
来自专栏算法与数据结构

利用代码计算原码,反码和补码

11430
来自专栏代码GG之家

一个通用的Java正则匹配工具

一个通用且常用的Java正则匹配工具,用以检查邮箱名、电话号码、用户密>码、邮政编码等合法性。 验证Email @param email email地址,格式:...

38270
来自专栏owent

Boost.Spirit 初体验

使用代码生成代码是一件十分美妙的事情,于是有了各种代码生成器。但是生成代码,意味着要有对生成规则的分析和处理。 Boost.Spirit 就是这么一个语法分析...

38010
来自专栏AndroidTv

谈谈你对 Java 平台的理解声明提问正文

11940
来自专栏编程

养良好C语言编程风格,编优质C语言代码,这才是C语言的开始

个人c语言编程风格总结 总结一下我个人的编程风格及这样做的原因吧,其实是为了给实验室写一个统一的C语言编程规范才写的。首先声明,我下面提到的编程规范,是自己给自...

57650

扫码关注云+社区

领取腾讯云代金券