异常

  • 异常是指成员没有完成它的名称所宣称的行动
  • 当CLR检测到某个正在运行的.NET应用程序处于一种特殊的正常执行顺序被打断的状态时,会生成一个异常对象来表示这个错误,并将此对象在方法调用堆栈中向上传送。如果一个程序引发了一个异常却没有处理,CLR将会中断此进程。

throw和throw e

执行throw e后异常栈重置,认为重新抛出异常的地方(调用throw e语句的地方)是异常栈的起点执行throw并不会重置异常栈

System.Exception 类—-所有异常类的基类

Message readonly string 指出异常的原因
Data readonly IDictionary 引用一个键值对集合
Source r/w string 包含异常的程序集名称
StackTrace r string 包含异常之前调用所有方法和信息
TargetSite r MethodBase 包含抛出异常的方法
InnerException r Exception 如果当前异常时在处理一个异常时抛出的,则指出上个异常是什么可以使用Exception.GetBaseException来遍历异常链表.
HResult r/w int COM api 返回值维护.

CLR的”两轮遍历”异常处理策略

  • 当应用程序拥有多层嵌套的异常捕获结构时,如果最底层或中间层发生了异常,CLR将优先在引发异常的那一层搜索catch语句块,看看有没有”兼容”此类型异常的处理代码,如果没有,”跳到”上一层去搜索,如果上一层还没有,继续往上搜索,由此直到方法调用堆栈的最顶层。这就是CLR异常处理策略的第一轮遍历:查找合适的异常处理程序。

  • 如果在某一层找到了异常处理程序,注意,CLR并不会马上执行之,而是回到”事故现场”,再次进行第二轮遍历,执行所有”中间”层次的finally语句块,然后,执行找到的异常处理程序,最后,再从本层开始一直遍历到最顶层,执行所有的finally语句块。但是有一个问题,如果始终没有找到合适的异常处理程序会怎么样,你试试就知道了。

catch的”诱惑”

有的程序员害怕让用户看到异常的出现,于是把所有的异常隐藏起来。这种作法其实是应用了一种”鸵鸟”策略,据说当有危险降临而又无法挣脱时,鸵鸟就会将头插入沙中,欺骗自己安全了。现实应用中不建议采取这样的做法来处理异常。

  1. try
  2. {
  3. //功能代码
  4. }
  5. catch(Eexception ex)
  6. {
  7. //在此处"吃掉"所有异常,没有任何代码进行异常处理
  8. }

CLR异常处理机制探秘

方法的异常处理表

  1. static void Main(string[] args)
  2. {
  3. try
  4. {
  5. int number = Convert.ToInt32(Console.ReadLine());
  6. }
  7. catch (FormatException ex)
  8. {
  9. Console.WriteLine(ex.Message);
  10. }
  11. finally
  12. {
  13. Console.WriteLine("finally");
  14. }
  15. }
  • 使用ildasm工具反编译出来的代码框架如下所示

异常和状态管理 - 图1

  • 从上述代码中可知:

    C#编程语言中的单层的try.catch.finally结构会被转换为”两层嵌套”的类似结构,CLR通过执行leave指令在IL汇编程序的try、catch和finally指令块间跳转,实现所定义的异常捕获和处理逻辑。

  • 单击ildasm的”view”菜单,取消”expand try/catch”选项,可以看到C#编译器生成的IL代码的真面目。

异常和状态管理 - 图2

具体功能代码被统一地放置在方法IL代码的前半部分,而用于实现异常捕获的代码放在方法IL代码的后半部分,称为”异常处理表”,”ret”指令是两部分的分界线。C#编译器通过在合适的地方插入leave指令使得其在无异常的情况下,永远执行不到异常处理代码。异常处理表中的每一项代表一个异常处理子句,IL汇编程序使用try、catch、handler和finally关键字,配合相应地址对前面的功能代码自然分块。

CLR如何捕获并处理异常

  • CLR获取引发异常的IL指令地址,然后从上到下地扫描异常处理表,取出每个catch子句”.try”关键字后面跟着的用于定位”块”的起始和结束地址,判断一下引发异常的IL指令地址是否落入到此地址范围中,如果中,取出”.catch”关键字后跟着的异常类型,比对一下是否与抛出的异常类型一致或相兼容,如果这个条件得到满足,CLR取出”.handler”后的两个IL地址,”准备”执行这两个地址范畴的IL指令。”扫描并查找相匹配的catch子句”过程,是CLR异常处理流程的第一轮。

  • 当找到了合适的异常处理代码后,CLR再”回到原地”,再次扫描引发异常方法所包容的异常处理表,这回CLR关注的不再是catch子句,而是finally子句,如果找到了合适的finally子句,CLR执行finally子句所指令的处理指令。”扫描并查找相匹配的finally子句”过程,是CLR处理异常流程的第二轮

约束执行区域(CER)

  1. private static void Demo1() {
  2. try {
  3. Console.WriteLine("In try");
  4. }
  5. finally {
  6. // Type1’s static constructor is implicitly called in here
  7. Type1.M();
  8. }
  9. }
  10. private sealed class Type1 {
  11. static Type1() {
  12. // if this throws an exception, M won’t get called
  13. Console.WriteLine("Type1's static ctor called");
  14. }
  15. public static void M() { }
  16. }
  • When I run the preceding code, I get the following output:

In try Type1’s static ctor called

  1. private static void Demo2() {
  2. // Force the code in the finally to be eagerly prepared
  3. RuntimeHelpers.PrepareConstrainedRegions(); // System.Runtime.CompilerServices namespace
  4. try {
  5. Console.WriteLine("In try");
  6. }
  7. finally {
  8. // Type2’s static constructor is implicitly called in here
  9. Type2.M();
  10. }
  11. }
  12. public class Type2 {
  13. static Type2() {
  14. Console.WriteLine("Type2's static ctor called");
  15. }
  16. // Use this attribute defined in the System.Runtime.ConstrainedExecution namespace
  17. [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
  18. public static void M() { }
  19. }
  • Now, when I run this version of the code, I get the following output.

Type2’s static ctor called In try

总结

  • PrepareConstrainedRegions是一个很特别的方法。JIT编译器如果发现在一个try块之前调用了这个方法,就会提前编译与try关联的catch和finally块中的代码。JIT编译器会加载任何程序集,创建任何类型对象,调用任何静态构造器,并对任何方法进行 т编译。如果其中任何操作造成异常,这个异常会在线程进入try块之前发生。

  • JIT编译器提前准备方法时,还会遍历整个调用图,提前准备被调用的方法,前提是这些方法应用了ReliabilityContractAttribute,而且向这个特性实例的构造器传递的是Consistency.WillNotCorruptState或者Consistency.MayCorruptInstance枚举成员。这是由于假如方法会损坏AppDomain或进程的状态,CLR便无法对状态一致性做出任何保证。在通过一个PrepareConstrainedRegions调用来保护的一个catch或finally块中,请确保只调用根据刚才的描述设置了ReliabilityContractAttribute的方法。

代码协定

代码协定(code contract)提供了直接在代码中声明代码设计决策的一种方式

  • 前条件

一般用于对实参进行验证。

  • 后条件

方法因为一次普通的返回或者抛出异常而终止时,对状态进行验证。

  • 对象不变性(Object Invariant)
    • 在对象的整个生命期内,确保对象的字段的良好状态。

代码协定的核心是静态类:System.Diagnostics.Contracts.Contract:

  1. public static class Contract
  2. {
  3. // Precondition methods: [Conditional("CONTRACTS_FULL")]
  4. public static void Requires(Boolean condition);
  5. public static void EndContractBlock();
  6. // Preconditions: Always
  7. public static void Requires<TException>(Boolean condition) where TException : Exception;
  8. // Postcondition methods: [Conditional("CONTRACTS_FULL")]
  9. public static void Ensures(Boolean condition);
  10. public static void EnsuresOnThrow<TException>(Boolean condition)
  11. where TException : Exception;
  12. // Special Postcondition methods: Always
  13. public static T Result<T>();
  14. public static T OldValue<T>(T value);
  15. public static T ValueAtReturn<T>(out T value);
  16. // Object Invariant methods: [Conditional("CONTRACTS_FULL")]
  17. public static void Invariant(Boolean condition);
  18. // Quantifier methods: Always
  19. public static Boolean Exists<T>(IEnumerable<T> collection, Predicate<T> predicate);
  20. public static Boolean Exists(Int32 fromInclusive, Int32 toExclusive,
  21. Predicate<Int32> predicate);
  22. public static Boolean ForAll<T>(IEnumerable<T> collection, Predicate<T> predicate);
  23. public static Boolean ForAll(Int32 fromInclusive, Int32 toExclusive,
  24. Predicate<Int32> predicate);
  25. // Helper methods: [Conditional("CONTRACTS_FULL")] or [Conditional("DEBUG")]
  26. public static void Assert(Boolean condition);
  27. public static void Assume(Boolean condition);
  28. // Infrastructure event: usually your code will not use this event
  29. public static event EventHandler<ContractFailedEventArgs> ContractFailed;
  30. }