CLR Via C# 3rd 阅读摘要 -- Chapter 20 – Exceptions and State Management
Defining "Exception"
- 异常顾名思义就是当一个成员在完成一个任务时出现了错误。.NET提供了异常处理的机制;
- 别错误的用返回值来表示异常情况。
Exception-Handing Mechanics
- .NET框架的异常处理机制是建立在Windows系统的SEH(Structured Excption Handling)基础上的;参考:《Windows via C/C++ 5th ed》
- try ... catch ... finally ... ;
- 如果在catch/finally块中不小心抛出了异常,CLR的异常机制开始执行就好像异常是在finally之后抛出的。这将导致CLR中止进程;
- 在CLR 2.0中,当一个CLR不兼容的异常抛出时,CLR自动构造一个RuntimeWrappedException实例,并设置它的私有字段指向实际抛出的对象;
- 可以使用RuntimeCompatiblityAttribute标记WrapNonExceptionThrows = false禁止CLR包装不兼容的异常。
The System.Exception Class
- throw ex; 与 throw;的区别,当抛出异常时,前者会导致CLR重置异常的开始点,而后者不会;
- 在异常处理时,如果想知道完整的堆栈踪迹信息,可以使用System.Diagnostics.StackTrace;
- 获得堆栈踪迹时,有些实际调用堆栈中的方法不显示在堆栈踪迹字串中。这有两个原因:
- 1. 堆栈只有线程返回的记录,而没有线程从哪来的记录;
- 2. JIT内联编译了方法。如果/debug开关打开,JIT就不会内联编译。
- 通过使用MethodImpl属性MethodImplOptions.NoInlining标记方法可以禁止JIT对该方法内联编译。
FCL-Defined Exception Class
- FCL的异常最终都继承自System.Exception。两个基本的异常:SystemException与ApplicationException。
Throwing an Exception
- 要抛出有意义的异常,决不要抛出类似System.Exception这样宽泛的异常;
- 设计异常时,要注意异常的描述,另外,没有必要本地化异常字串消息。
Defining Your Own Exception Class
- 设计自己的异常既枯燥又易错,所以尽量使用现有的异常;
- 设计一个泛型的异常?个人觉得没必要。
Trading Reliability for Productivity
- 如何减缓状态腐败:
- CLR不允许在执行catch或finally中的代码时中止线程;但是不推荐在finally中写事务性代码;
- 可以使用System.Diagnostics.Contracts.Contract在方法上进行代码约束;
- 使用CER(Constrained Execution Regions);
- 使用事务System.Transactions.TransactionScope来确保所有状态一起改变或者不变;
- 显示的使用Monitor.Enter, Exit。
- 如果感觉状态太糟糕需要中止进程,可以使用Enviroment.FailFast方法;
- Enviroment.FailFast方法不会在运行finally块中的代码以及Finalize方法,但是允许继承自CriticalFinalizeObject的对象有机会完成清理;
- Enviroment.FailFast会在Windows的应用程序日志中记录日志。
Guidelines and Best Practices
- 合理的使用finally块。lock, using, foreach会自动生成try/finally块;
- 不要catch所有的异常,永远不要把异常偷偷吃了;
- 从异常中优雅的恢复;
- 管理状态--在不可恢复的异常发生时停止部分完成操作;
- 隐藏实现的细节来维护“契约”。可以在.Data属性中添加额外信息。
- 当一个类型构造器抛出异常没有被处理,CLR会在内部俘获并抛出一个新的TypeInitializationException替代;
- 当使用反射的方法抛出异常时,CLR会俘获然后抛出TargetInvocationException替代。最好在代码中throw ex.InnerException重新抛出;
- dynamic比反射要管用。
Unhandled Exceptions
- SilverLight程序不执行System.AppDomain.UnhandledException事件因为权限问题;
- SilverLight程序,关照System.Application.UnhandledException事件;
- Windows Form程序,关照System.Windows.Forms.NativeWindow.OnThreadException虚方法, System.Windows.Forms.Application.OnThreadException虚方法, .ThreadException事件;
- WPF,关照System.Windows.Application.DispatcherUnhandledException事件,System.Windows.Threading.Dispatcher.UnHandledException, UnHandledExceptionFilter事件;
- ASP.NET Web Form程序,关照System.Web.UI.TemplateControl.Error事件。以后还有System.Web.HttpApplication.Error事件;
- WCF程序,关照System.ServiceModel.Dispatcher.ChannelDispatcher.ErrorHandlers属性。
- 本地代码抛出的异常,CLR视为CESs(Corrupted State Exceptions)。
Debugging Exceptions
- 可以在VS IDE的异常选项中设置哪些异常发生时会中断(C++, CLR, Managed Debugging Assistants, Native Run-Time Checks, Windows32)。
Exception-Handling Performance Considerations
- 工具:Performance Monitor;
- TryXXX的方法要慢一点点。
Constrained Execution Regions(CERs)
- 为什么要有CER?对一些应用,比如DBMS,由于异常的原因造成任何管理状态和数据的丢失可能都是灾难性的;
- CLR的AppDomain是包含状态的,如果AppDomain卸载了,相应的状态也卸载;
- CER的定义:能够在错误时复原的代码块;因为AppDomain可以卸载并销毁它们的状态,CERs通常用于管理那些被多个AppDomain或进程共享的状态;
- RuntimeHelper.PrepareConstrainedRegions();强制catch/finally中的代码如果有CER约束([ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)])的行为在进入try之前先执行;
- 如果在catch或finally代码块中出现异步异常(不可预料的异常,比如当调用一个方法时,CLR要装载程序集,在AppDomain的装载器堆中创建类型对象,调用类型的静态构造器,JIT编译IL到本地代码,等等,这其中都可能出现错误。),错误恢复或者清理代码就不能作为整体执行;
- enum Cer {None, MayFail, Success};
- enum Consistency {MayCorruptProcess, MayCorruptAppDomain, MayCorruptInstance, WillNotCorruptState};
- 如果希望你写的方法保证不会毁坏任何状态,那么就要用属性标记为Consistency.WillNotCorruptState。
Code Contracts
- Preconditions, Postconditions, Object Invariants;
- 工具:
- 自动测试工具:Pex;
- 代码契约工具(IDE插件):Code Contracts AddIn;
- 代码契约重写工具:CCRewrite.exe;
- 代码契约检查工具:CCCheck.exe;
- 代码契约引用程序集生成工具:CCRefGen.exe;
- 代码契约文档生成工具:CCDocGen.exe。
- System.Dianostics.Contracts.Contract -> Conditional;方法:Requires,Requires
,Ensures,EnsureOnThrow,Invarint,Assert,Assume; - 所有在前置条件,后者条件,不变性测试中引用的成员必须不受副作用的影响。也就是测试条件不能改变他们的值;
- 关于继承,所有的继承类型不能重载或改变定义在基类中的虚拟方法的前置条件;
- 如果方法中有多个返回点,CCRewrite工具修改所有相应的IL代码在返回前执行后置条件检查;
- CCRewrite检查类型中所有标记有属性[ContractInvariantMethod]的方法。习惯通常采用ObjectInvariant的方法名并且使用private修饰,没有参数和返回值。CCRewrite会在每一个public的实例方法的末尾中插入IL代码来调用ObjectInvariant方法;
- Assert与Assume的区别:当用CCCheck.exe分析代码时,由于一些对象没有实例信息,所以Assert会有警告,而Assume没这问题;
- 使用CCRewrite.exe可以帮助你快速发现BUG,但是产生的代码使得程序集变的很大,这将严重影响性能。这时就可以用CCRefGen.exe来创建分离的契约引用程序集;
- CCRefGen.exe生成的契约程序集通常名称为AssemblyName.Contracts.dll。
本章小结
本章的主要内容是关于错误处理和如何增强可靠性的。首先,定义了什么是错误,强调不能用异常来做流程处理。然后,讲了如何发现错误并进行处理。解释了CLR异常处理的机制,详细讲了System.Exception基础,FCL的异常类,以及如何正确的抛出异常。还演示了如何定义自己的异常,这是一个比较单调易错的工作。接着讲了如何提高生产效率并增强可靠性,给出了使用异常的最佳实践指南。然后讲了如何对待未处理的异常,以及如何调试异常。分析了异常处理的性能问题,阐述了CER的概念和用途。最后讲解了如何使用代码契约来提供程序的鲁棒性,并推荐了一些常用的工具。