Loading

浅析CLR的异常处理模型

文章目录:

  1. 异常概述
  2. CLR中的异常处理机制
  3. CLR中异常的核心类System.Exception类
  4. 异常处理的设计规范和最佳实践
  5. 异常处理的性能问题
  6. 其他拓展

1、异常概述

  异常我们通常指的是行动成员(例如类实例对象)没有完成所宣称的行动或任务。

  例如下图中代码,返回 "Lmc"这个字符串的第二个字符的大写是否为 "M",假如这个执行过程中任何一个步骤出错,都应该返回一个状态(例如"L".Substring(1,1)会因为字符串索引不够长而出现异常),指示代码不能正常进行完成行动,但是以下这句代码是没有办法返回的,所以.net framework 使用异常处理来解决这个问题,抛出特定异常("L".Substring(1,1)会抛出ArgumentOutOfRangeException异常)。

2、CLR中的异常处理机制

  C#中的异常处理机制是使用try , catch ,finally关键字来包裹代码,捕获异常,以及执行恢复清理操作。使用规范是try块中写入正常执行/需要清理的代码,catch块捕获特定异常,执行回复操作,finally块执行清理代码。

  其中catch块会优先捕捉特定的异常。例如try块抛出异常,CLR会搜索与try块同级的,捕捉类型与throw类型相同的的catch块,假如没有找到,CLR会调用栈更高的一层去搜索与异常类型相匹配的catch块。假如到了调用栈顶部,依旧没有找到匹配的catch块,就会发生无处里的异常。

  当CLR找到匹配的catch块,就会执行内层所有finally块代码,然后执行catch块,执行与捕获catch块相同级的finally代码。 如下如所示:

 1         private static void Exfun1()
 2         {
 3             try
 4             {
 5                 Exfun2();
 6             }
 7             catch(Exception ex)
 8             {
 9                 Console.WriteLine($" this is Exfun1  Exception : {ex.StackTrace}");  //3 
10             }
11             finally
12             {
13                 Console.WriteLine("this is Exfun1 finally");  //4
14             }
15         }
16         private static void Exfun2()
17         {
18             try
19             {
20                 Console.WriteLine("this is Exfun2");   //1 
21                 throw new IOException();
22             }
23             catch(InvalidCastException ex)
24             {
25                 Console.WriteLine($"this is Exfun2 InvalidCastException {ex.Message}");  //由于捕获的异常与抛出的异常不匹配,所以不执行
26             }
27             finally
28             {
29                 Console.WriteLine("this is Exfun2 finally"); //2  由于是在Exfun1中的catch捕获到异常,所以先执行内层的catch块。
30             }
31         }
View Code

  在catch块的结尾,我们有三个选择:

    • 重新抛出相同异常
    • 抛出一个不同的异常  
    • 让线程从catch块底部退出(把异常吞掉)  

  finally块执行与try块中行动需要的资源清理操作。(例如try块中打开了一个数据库连接,finally块中执行sqlconnection.close();sqlconnection.dispose();)

  catch块和finally块中的代码应该非常短,而且具有很高的执行成功率,避免catch块和finally块中代码再次抛出异常。当出现异常直至调用栈顶部都没有正确的catch捕获,就会产生一个未处理的异常,这时CLR会终止执行的进程,保护数据被进一步损坏。

3、CLR中异常的核心类System.Exception类

  CLR中允许异常抛出任意类型,例如int string,但是根据CLS(公共语言规范),C#只能抛出派生自System.Exception的类。

  当一个异常抛出被catch块捕捉时,CLR会记录catch捕获的位置,CLR会创建一个字符串赋值给Exception类的StackTrace属性。catch块中重新抛出捕获的异常会导致CLR重置异常起点。例如:

 1        private static void SomeMehtod()
 2         {
 3             try
 4             {
 5                 Console.WriteLine("this is someMthod1");
 6                 SomeMethod2();
 7             }
 8             catch (Exception e)
 9             {
10                 Console.WriteLine($"method1 reset exception line {e.StackTrace}");
11             }
12         }
13         private static void SomeMethod2()
14         {
15             try
16             {
17                 Console.WriteLine("this is someMthod2");
18                 throw new IOException();
19             }
20             catch (IOException e)
21             {
22                 Console.WriteLine($"method2 exception line {e.StackTrace}");
23                 throw e;
24             }
25         }
异常位置重置

  假如想较准确知道错误位置,可以使用如下写法:

 1         private void SomeMethodNoReset()
 2         {
 3             bool trySucceeds = false;
 4             try
 5             {
 6                 //dosomething
 7                 trySucceeds = true;
 8             }
 9             finally
10             {
11                 if (!trySucceeds)
12                 {
13                    
14                 }
15             }
16         }
View Code

  对于系统抛出异常,可以向AppDomain的FirstChanceException事件登记,这样,只要在这个Appdomain(应用程序域)中发生异常,就可以得到通知:

 1         static void Main(string[] args)
 2         {
 3             var thisdomain = Thread.GetDomain();
 4             thisdomain.FirstChanceException += Thisdomain_FirstChanceException;
 5             Exfun1();
 6         ....
 7         }            
 8         private static void Thisdomain_FirstChanceException(object sender, System.Runtime.ExceptionServices.FirstChanceExceptionEventArgs e)
 9         {
10             Console.WriteLine($"appdomain 中FirstChanceException事件登记发生的异常{e.Exception.Message}");
11         }
FirstChanceException

  当方法无法完成指明的任务的时候,就应该抛出一个异常。抛出异常时应该注意2点:1、抛出的异常应该是一个有意义的类型建议使用宽而浅的异常类,尽量少的使用基类。2、向异常类传递的信息应该指明为什么无法完成任务,帮助开发人员修正代码。

   以下是使用反射加载的Exception的类以及子类的部分截图

 1 private static void Go()
 2         {
 3             LoadAssemblies();
 4             var allTypes = (from a in AppDomain.CurrentDomain.GetAssemblies()
 5                             from t in a.ExportedTypes
 6                             where typeof(Exception).GetTypeInfo().IsAssignableFrom(t.GetTypeInfo())
 7                             orderby t.Name
 8                             select t).ToArray();
 9             Console.WriteLine(WalkInherirtanceHierarchy(new StringBuilder(), 0, typeof(Exception), allTypes));
10         }
11         private static StringBuilder WalkInherirtanceHierarchy(StringBuilder sb, int indent, Type baseType, IEnumerable<Type> allTypes)
12         {
13             string spaces = new string(' ', indent * 3);
14             sb.AppendLine(spaces + baseType.FullName);
15             foreach (var t in allTypes)
16             {
17                 if (t.GetTypeInfo().BaseType != baseType) continue;
18                 WalkInherirtanceHierarchy(sb, indent + 1, t, allTypes);
19             }
20             return sb;
21         }
22         private static void LoadAssemblies()
23         {
24             String[] assemblies = {
25             "System,                        PublicKeyToken={0}",
26             "System.Core,                   PublicKeyToken={0}",
27             "System.Data,                   PublicKeyToken={0}",
28             "System.Design,                 PublicKeyToken={1}",
29             "System.DirectoryServices,      PublicKeyToken={1}",
30             "System.Drawing,                PublicKeyToken={1}",
31             "System.Drawing.Design,         PublicKeyToken={1}",
32             "System.Management,             PublicKeyToken={1}",
33             "System.Messaging,              PublicKeyToken={1}",
34             "System.Runtime.Remoting,       PublicKeyToken={0}",
35             "System.Runtime.Serialization,  PublicKeyToken={0}",
36             "System.Security,               PublicKeyToken={1}",
37             "System.ServiceModel,           PublicKeyToken={0}",
38             "System.ServiceProcess,         PublicKeyToken={1}",
39             "System.Web,                    PublicKeyToken={1}",
40             "System.Web.RegularExpressions, PublicKeyToken={1}",
41             "System.Web.Services,           PublicKeyToken={1}",
42             "System.Xml,                    PublicKeyToken={0}",
43             "System.Xml.Linq,               PublicKeyToken={0}",
44             "Microsoft.CSharp,              PublicKeyToken={1}",
45          };
46 
47             const String EcmaPublicKeyToken = "b77a5c561934e089";
48             const String MSPublicKeyToken = "b03f5f7f11d50a3a";
49             
50             Version version = typeof(System.Object).Assembly.GetName().Version;
51             
52             foreach (String a in assemblies)
53             {
54                 String AssemblyIdentity =
55                    String.Format(a, EcmaPublicKeyToken, MSPublicKeyToken) +
56                       ", Culture=neutral, Version=" + version;
57                 Assembly.Load(AssemblyIdentity);
58             }
59         }
Exception以及子类

 4、异常处理的设计规范和最佳实践

  1. 善用finally块,在执行catch块和finally块中的代码的时候,CLR不允许线程终止。所以,finally块中代码始终会执行,应该先用finally块清理那些已经成功启动的操作,再返回至调用者或者执行finally块之后的代码;利用finally块中代码显示释放对象避免资源泄露。
    • 例如使用lock语句,锁将在finally块中被释放。
    • 使用using语句时候,finally块中调用对象的Dispose方法。
    • foreach语句,再finally方法中调用IEnumerator对象的Dispose方法。
    • 析构方法,在finally块中调用基类的Finalize方法。
  2. 不要什么都捕捉,不要过于频繁的,不恰当的使用catch块。不要把异常吞噬掉,而是应该允许一场在调用栈中向上移动,让应用程序代码针对性处理。
  3. 得体的从异常中恢复。
  4. 发生不可恢复的异常时,回滚部分完成的操作来维持状态。
    • 例如要序列化一组对象到磁盘文件,当中途失败时,要文件回滚到对象序列化之前的状态。
  5. 隐藏实现细节来维系协定;例如现在有一个获取用户电话号码的功能,通过输入名字,从文件中找到匹配号码并返回。假如文件不存在或者文件读取异常,这时候就不应该将这两个异常信息返回给用户,应该返回一个自定义的用户尚未找到该用户的号码这样的异常给调用者。 以下是伪代码:
     1 public sealed class PhoneBook
     2     {
     3         private string m_pathname; //地址簿文件路径名称
     4         public string GetPhoneNumber(string name)
     5         {
     6             string phone;
     7             FileStream fileStream = null;
     8             try
     9             {
    10                 //根据name从fs中读取内容
    11                 fileStream = new FileStream(m_pathname, FileMode.Open);
    12                 byte[] bt = new byte[1000];
    13                 fileStream.Read(bt, 0, 123);
    14                 phone = System.Text.Encoding.Default.GetString(bt);
    15                 return phone;
    16             }
    17             catch(FileNotFoundException ex)
    18             {
    19                 //重新抛出一个不同的异常,而且加入name
    20                 //将原来的异常设置为内部异常
    21                 throw new NameNotFoundException(name, ex);
    22             }
    23             catch(IOException ex)
    24             {
    25                 throw new NameNotFoundException(name, ex);
    26             }
    27         }
    28     }
    29     public class NameNotFoundException : Exception {
    30         public NameNotFoundException(string name,Exception e) { }
    31     }
    View Code
  6.  对于未处理的异常会造成进程终止,这些异常可以在windows日志中查看。具体位置为事件管理器->windows日志->应用程序。

5、异常处理的性能问题

  对于非托管代码,例如C++,编译器必须生成代码来跟踪有哪些对象被成功构造。编译器还要生成代码在异常被捕捉时候来调用已成功构造的对象的析构器。这会在应用程序生成大量的簿记代码,影响代码的大小和执行时间;

  对于托管代码,例如C#,因为托管对象是在托管堆中分配内存,所以这些对象受到GC的监控。如果对象被成功构造且抛出异常,将会由GC来释放对象内存。编译器不用生成簿记代码来跟踪成功构造对象,也不用由编译器保证对象析构器的调用。

  在遇到频繁调用且频繁失败的方法,这时候抛出异常会造成巨大的性能损失。这时候在方法中可以使用FCL提供的TryXxx方法。例如 int 的 TryParse。

6、其他拓展(CER)

  CER(约束执行区域)是必须对错误有适应力的代码块。在CLR的代码执行过程中,可能由于AppDomain中的一个线程遇到未处理的异常从而导致进程中的整个AppDomain遭到卸载。AppDomain卸载时它的所有状态都会卸载。所以CER一般用于处理多个AppDomain或进程共享的状态。例如,当调用一个类型的静态构造器时,可能抛出异常。这时候,假如是在catch块或者finally块中,错误恢复代码和资源清理代码就不能完整的执行。如下图所示:因为调用Type1的M方法时候,会隐式调用M的静态构造器,这样finally中的代码就不能完整的执行。

  

  解决方案是使用CER,CER使用方法是在try块代码前添加 RuntimeHelpers.PrepareConstrainedRegions(); 在finlly块执行的方法用ReliabilityContract特性修饰。这样,JIT编译器会提前编译与try块关联的catch块和finlly块的代码。并且会加载相应程序集,调用静态构造器。JIT编译器还会遍历调用图,提前准备用ReliabilityContract修饰的方法。

  

 

 

  

posted @ 2018-06-03 18:12  3WLineCode  阅读(197)  评论(0编辑  收藏  举报