当你跑起了一个异步线程,并用 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

使用 ExceptionDispatchInfo 捕捉并重新抛出异常的更多相关文章

  1. 2019-10-7-WPF-如何跨线程重新抛出异常

    title author date CreateTime categories WPF 如何跨线程重新抛出异常 lindexi 2019-10-07 13:24:54 +0800 2019-10-4 ...

  2. 应该抛出什么异常?不应该抛出什么异常?(.NET/C#)

    我在 .NET/C# 建议的异常处理原则 中描述了如何 catch 异常以及重新 throw.然而何时应该 throw 异常,以及应该 throw 什么异常呢? 究竟是谁错了? 代码中从上到下从里到外 ...

  3. 【C++】 C++异常捕捉和处理

    在阅读别人开发的项目中,也许你会经常看到了多处使用异常的代码,也许你也很少遇见使用异常处理的代码.那在什么时候该使用异常,又在什么时候不该使用异常呢?在学习完异常基本概念和语法之后,后面会有讲解. ( ...

  4. python虚拟机中的异常流控制

    异常:对程序运行中的非正常情况进行抽象.并且提供相应的语法结构和语义元素,使得程序员能够通过这些语法结构和语义元素来方便地描述异常发生时的行为. 1.Python中的异常机制: 1.1Python虚拟 ...

  5. Java之异常处理机制

    来源:深入理解java异常处理机制 2.Java异常    异常指不期而至的各种状况,如:文件找不到.网络连接失败.非法参数等.异常是一个事件,它发生在程序运行期间,干扰了正常的指令流程.Java通 ...

  6. [R]R语言里的异常处理与错误控制

    之前一直只是在写小程序脚本工具,几乎不会对异常和错误进行控制和处理. 随着脚本结构和逻辑更复杂,脚本输出结果的准确性验证困难,同时已发布脚本的维护也变得困难.所以也开始考虑引入异常处理和测试工具的事情 ...

  7. 再探Java基础——throw与throws

    http://blog.csdn.net/luoweifu/article/details/10721543 异常处理机制 异常处理是对可能出现的异常进行处理,以防止程序遇到异常时被卡死,处于一直等待 ...

  8. [swift]可选类型

    可选类型 <Swift权威指南>第2章千里之行始于足下——Swift语言基础,本章挑选了Swift语言的最基本特性加以介绍.尽管这些特性只占Swift全部特性的很少一部分,但却是所有的Sw ...

  9. 全面理解Java异常的运行机制

    1. 引子 try…catch…finally恐怕是大家再熟悉不过的语句了,而且感觉用起来也是很简单,逻辑上似乎也是很容易理解.不过,我亲自体验的“教训”告诉我,这个东西可不是想象中的那么简单.听话. ...

随机推荐

  1. Linux安装keepalived

    1.下载安装ipvs安装包,进行解压 http://www.keepalived.org/software/ 2.创建安装路径连接 安装环境: yum -y install openssl-devel ...

  2. Apache的MaxClients设置

    本文将介绍Apache的MaxClients参数的重要性以及在GC发生时对系统整体性能的显著影响.通过几个例子,你将会更清晰的理解MaxClients值所引发的问题.最后会介绍如何依据系统的可用内存来 ...

  3. angular2 自定义双向绑定属性

    import { Component, OnInit, Output, Input, EventEmitter } from '@angular/core'; @Component({ selecto ...

  4. 如何高效的使用 Git

    -- 代码昨天还是运行好好的今天就不行了. 代码被删了. 突然出现了一个奇怪的 bug,但是没人知道怎么回事. 如果你出现过上面的任何一种情况,那本篇文章就是为你准备的. 除了知道 git add, ...

  5. Android数据库框架-----GreenDao3的相关使用和版本升级更新

    GreenDAO是一款非要流行的Android平台上的数据库框架,性能优秀,代码简洁:是一个将对象映射到SQLite数据库中的轻量且快速的ORM解决方案. GreenDAO 优势 1.一个精简的库 2 ...

  6. spring boot 中logback多环境配置

    spring boot 配置logback spring boot自带了log打印功能,使用的是Commons logging 具体可以参考spring boot log 因此,我们只需要在resou ...

  7. jquery基础 笔记一

    一. 1. vsdoc: 在Visual Studio中需要引入此版本的jquery类库才能启用智能感知.如:jquery-1.3.2-vsdoc2.js<body> <div id ...

  8. Python 读取window下UTF-8-BOM 文件

    with open('target.txt', 'r', encoding='utf_8_sig') as fp: print(fp.read())

  9. RabbitMQ(2) vhost、用户和权限

    rabbitmq中,vhost提供了资源隔离机制.同时,通过为用户设置vhost粒度的权限,可以实现丰富的权限控制功能. vhost 在rabbitmq中,可以创建被称为虚拟主机(vhost)的虚拟消 ...

  10. linux系统挂载NTFS移动硬盘

    有时候做大数据量迁移时,为了快速迁移大数据,有可能在Linux服务器上临时挂载NTFS格式的移动硬盘, 一般情况下,Linux是识别不了NTFS格式移动硬盘的(需要重编译Linux核心才能,加挂NTF ...