.Net中的异常处理:高级异常
这种处理的有效性主要取决于所选择的语言和平台,因此,详细了解它们的正确用法和行为非常重要,这样我们的用户和其他开发人员在诊断代码中的问题时免受痛苦。
在本文中,我们将了解C和.NET在错误处理方面的作用。
词汇表
CLR:公共语言运行时的缩写,是.NET运行时,它负责执行用所有.NET语言编译的应用程序。除了虚拟机和实时编译器之外,它还具有额外的职责,如内存管理、安全性等。
BCL:Base Class Library的缩写,是.NET framework的核心库。除了直接使用CLR操作之外,它还公开了原始数据类型和构建和运行应用程序的基本功能。也称为mscorlib。
FCL:Framework类库的缩写,是我们大多数人在.NET中所知道的“框架”。使用BCL作为构建块,它公开了大量具有各种特性的名称空间,比如系统IO, 系统安全, 系统文本,等等。
TPL:Task Parallel Library的缩写,是一个包含由异步关键字和API提供的功能的库。它是随着.NET版本4.5和C#5一起发布的。
SEH:Structured Exception Handling的缩写,是Windows的原生异常子系统,它在操作系统级别处理软件和硬件异常
MDA:托管调试助手的缩写。这些是特殊的调试扩展,向VisualStudio调试器提供与CLR执行状态相关的信息,后者由内部助手和资产公开。
重新审查异常
在.NET中,尤其是在C#中,异常是使用try、catch和finally块来处理的。首先,try块将包含预期引发异常的代码,其次是catch块,它将指定异常类型和在try块内引发与指定类型匹配的异常时将执行的代码块:
Random rnd = new Random(); try { Console.WriteLine(1 / rnd.Next(-100, 101)); } catch (DivideByZeroException ex) { Debug.WriteLine(“A division by zero attempt has occurred”); }
值得注意的是,异常类型catch将处理所有可能的异常(这并不完全正确,但我们现在将坚持这个假设):
try { //... } catch (Exception ex) { //General exception handler }
编写通用异常处理程序的另一种方法是:
catch { //... }
try块可以有多个catch块,这些catch块与不同的异常类型相关联,这些异常块将根据类层次结构按降序进行求值:
catch (DivideByZeroException ex) { //Code that handles division by zero exceptions } catch (Exception ex) { //Code that handles any exception that may occur }
注意:记住异常类型的求值顺序很重要,因为如果我们的第一个catch处理一个异常类型,那么下面所有的异常处理程序都将被忽略。这是一个常见的错误,以后在复杂的应用程序中可能很难检测到。
最后,我们有finally块(请原谅冗余),它将始终执行其包含的代码,无论是否抛出异常。因此,完整的异常处理程序具有以下形式:
try { //... } catch { //... } finally { //... }
值得注意的是,finally块中的代码不包括在异常处理中,因此在这里抛出的任何异常都不会被捕捉到,并且它会在调用堆栈中冒泡,直到它被管理为止。最后一个细节是,throw关键字在异常处理上下文中具有特殊的含义,因为它还允许我们指定如何传播捕获到的异常。基于前面的示例:
try { Console.WriteLine(1 / rnd.Next(-100, 101)); }
我们可以在catch块内以多种方式传播异常:
1 – Continue with the exception while preserving its stack trace, also called rethrow
2 – Throw the same type of exception but eliminating its original stack trace
3 – Throw a new exception, eliminating the original stack trace
4 – Throw a new exception, passing the current one as its InnerException
1
2
3
|
thrownewInvalidOperationException(ex);
}
|
C#中async的一个已知限制是await关键字不能在catch和finally块中使用。这个问题在C#6中得到了解决,而且,既然我们讨论的是C#6,我们将提到一个新的特性,即catch块中添加的异常过滤器。这种新的语法糖让我们根据表达式的布尔值捕获o传播异常:
try {…} catch (MyException e) when (filter(e)) {…}
其中一个主要的优点是我们可以在不改变堆栈跟踪的情况下管理异常流。它还可以用作拦截器,以添加日志等副作用:
private static bool Log(Exception e) { /* exception logging */; return false; //This preserves the call stack } try {…} catch (Exception e) when (Log(e)) {...}
我们仍然需要记住之前看到的例外顺序,以避免出现以下问题:
catch (MyException ex) { // Compilation error, will catch all instances of MyException } catch (MyException ex) when (filter1(ex) && filter2(ex)) { // Unreachable } catch (MyException ex) when (filter1(ex)) { // Unreachable }
.NET中的异常
下表显示了BCL中所有基本异常的列表
Exception type | Base type | Description |
Exception | Object | Base class for all exceptions. |
SystemException | Exception | Base class for all runtime-generated errors. |
IndexOutOfRangeException | SystemException | Thrown by the runtime only when an array is indexed improperly. |
NullReferenceException | SystemException | Thrown by the runtime only when a null object is referenced. |
AccessViolationException | SystemException | Thrown by the runtime only when invalid memory is accessed. |
InvalidOperationException | SystemException | Thrown by methods when in an invalid state. |
ArgumentException | SystemException | Base class for all argument exceptions. |
ArgumentNullException | ArgumentException | Thrown by methods that do not allow an argument to be null. |
ArgumentOutOfRangeException | ArgumentException | Thrown by methods that verify that arguments are in a given range. |
ExternalException | SystemException | Base class for exceptions that occur or are targeted at environments outside the runtime. |
COMException | ExternalException | Exception encapsulating COM HRESULT information. |
SEHException | ExternalException | Exception encapsulating Win32 structured exception handling information. |
我们还可以包括执行任务和异步代码(TPL和Parallel LINQ)产生的AggregateException类型。它是一种特殊的异常类型,因为它将多个异常合并到一个对象中,组成一个异常树。有关详细信息,请参阅MSDN上的tasks和PLINQ文章
在C#中,有几个关键字在生成的代码中包含异常处理:
- Using:生成一个try/finally,其中执行包含对象的Dispose方法。
- Async/await:异步代码被编译到一个状态机中,该状态机管理方法调用转换和委托,除其他外,它还生成catch块,聚集所有抛出的异常。
- Yield:与async类似,coroutines(通过Yield return语句在C中实现)还生成一个状态机及其各自的异常处理代码。
异常分发
自.NET4.5以来,作为TPL发行版的一部分,新特性之一是异常分派,它由命名空间的ExceptionDispatchInfo类提供System.Runtime.ExceptionServices.通过这个类,我们可以保留一个异常并将其委托给另一个实例,只要我们保持在同一个AppDomain中。稍后,我们可以检查异常并再次抛出它(rethrow):
ExceptionDispatchInfo exInfo = null; try { //... } catch (Exception ex) { exInfo = ExceptionDispatchInfo.Capture(ex); } //... if (exInfo != null) { exInfo.Throw(); }
高级异常:第一部分-CSE
到目前为止,我们一直假设无论发生什么,所有异常都将被它们关联的catch块无条件地捕获,然后,最后一个块将被执行。这还是真的吗?答案是否定的,至少不总是这样。根据设计,CLR有一些无法捕获的特殊异常类型,或者至少在不通过配置强制捕获的情况下无法捕获它们。
第一种是损坏状态异常(CSE)。它们由一组来自Win32/SEH的8个本机异常组成,由于它们的性质,它们假定它们不可管理,因为它们暗示程序处于无效、不一致和不可恢复的状态。
本机异常及其CLR对应项如下:
- EXCEPTION_ACCESS_VIOLATION – System.AccessViolationException
- EXCEPTION_STACK_OVERFLOW – System.StackOverflowException
- EXCEPTION_ILLEGAL_INSTRUCTION – SEHException
- EXCEPTION_IN_PAGE_ERROR – SEHException
- EXCEPTION_INVALID_DISPOSITION – SEHException
- EXCEPTION_NONCONTINUABLE_EXCEPTION – SEHException
- EXCEPTION_PRIV_INSTRUCTION – SEHException
- STATUS_UNWIND_CONSOLIDATE – SEHException
在此实例中,异常显示为“未处理的异常”,并且在visualstudio的本地调试中无法观察到这些异常。更详细地诊断这些问题的唯一方法是使用其SOS扩展和分析崩溃转储等。
它们被托管代码忽略的原因是,它们是来自本机代码的传入异常,它们逃避了应用程序的责任,或者,我们正在处理一个CLR错误,这种情况不太可能,但并非不可能。如果我们使用C的不安全特性来编写非托管代码(直接访问内存),也可能是代码中的错误造成的。
如果我们仍然希望处理这些异常,CLR让我们可以选择通过属性HandleProcessCorruptedStateExceptions捕捉其中的一些(而不是全部)。这要求方法具有SecurityCritical的访问级别:
[HandleProcessCorruptedStateExceptions] [SecurityCritical] public void MyMethod() { try { Marshall.StructureToPtr(1000, new IntPtr(1000), true); } catch {…} }
在我们的应用程序配置中,也可以通过将legacyCorruptedStateExceptionPolicy属性指定为true来实现。
注意:由于兼容性的原因,CSE将在即将到来的.netcore版本中被删除。由于桌面版本是Windows的原始版本,因此在桌面版本中这些内容将保持不变。
还有两种无法处理的异常类型,它们表示进程过早终止且不可恢复:StackOverflowExceptions(如果源于CLR)和CLR异常,我们将在下面看到。
高级异常:第二部分——CLR中的异常
深入研究异常的起源,我们偶然发现了CLR异常。运行时主要是用C++构建的,并且具有支配复杂场景的任务,即处理多个异常类型:
- 本机Windows异常(SEH):CLR将这些异常的处理与模拟VC++编译器内部函数的宏统一起来(“uu try”、“uu catch”等)。SEH模型非常复杂,例如,与Linux相比,在堆栈帧和二进制兼容性方面,展开(我们将在下一节中看到)是一个代价高昂的过程。
- C++异常:这些异常可以由CLR代码本身引发。
- CLR异常:这些异常是在执行期间由虚拟机委派并向应用程序公开的异常。
- corecrr是CLR的多平台和开源版本,它的任务更艰巨,那就是集成与多种平台和架构(Linux和macx64,以及其他正在开发的x86和ARM32/ARM64)的兼容性。为了达到所需的解耦级别,它依赖于一个称为PAL(平台适配层)的层。
如果CLR中发生严重错误,它将抛出ExecutionEngineException(不推荐)和fataxecutionengineerror(MDA)。这些错误通常表示托管代码中的堆损坏。
性能
为了完成异常之旅,我们将继续进行简短但必须讨论的性能含义。与普通代码的执行相比,异常处理非常昂贵。以下行为和副作用会导致异常情况:
- 由于执行流(以及上下文)的改变,在内存中缓存错误和页面错误。
- 展开:这是在try块中属于导致异常的代码的上下文被“清理”的过程。这涉及到遍历所有以前的堆栈帧以及释放/处置受影响的对象。此外,所有这些都可能导致垃圾回收器的下一次内存压缩。
- 诊断对象的附加分配(StackTrace)
- “冷”代码访问,这需要额外的时间进行即时编译(JIT)。
为了完成本文,我们将提供一些在设计和实施有效解决方案时有用的提示:
- 避免在应用程序中使用异常作为控制流。使用错误代码,或者更好的是,将错误和警告作为类设计的一部分来考虑。
- 以异常抛出和捕获来衡量:例如,在web场景中,异常处理可以集中在每个请求上,或者,在分层体系结构中,可以跨层对每次调用进行处理(依赖注入框架为此提供了几种工具)。
- 避免使用过多代码的try块,尤其是当抛出的代码不是立即相关或依赖于它时。这是因为大型代码体(在方法和块中)可以抢先地禁用由实时编译器(JIT)进行的运行时优化