文章目录:

  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"); //
  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. }

  在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. }

  对于系统抛出异常,可以向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(), , 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 * );
  14. sb.AppendLine(spaces + baseType.FullName);
  15. foreach (var t in allTypes)
  16. {
  17. if (t.GetTypeInfo().BaseType != baseType) continue;
  18. WalkInherirtanceHierarchy(sb, indent + , 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[];
    13. fileStream.Read(bt, , );
    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. }
  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修饰的方法。

  

  

浅析CLR的异常处理模型的更多相关文章

  1. 01.由浅入深学习.NET CLR 基础系列之CLR 的执行模型

    .Net 从代码生成到执行,这中间的一些列过程是一个有别于其他的新技术新概念,那么这是一个什么样的过程呢,有什么样的机制呢,清楚了这些基本的东西我们做.Net的东西方可心中有数.那么,CLR的执行模型 ...

  2. 第一章 CLR 的执行模型

    CLR via C# 读书笔记:第一章 CLR 的执行模型(1) 第Ⅰ部分CLR基础.这部分为三章(第一章:CLR的只想能够模型,第二章:生成.打包.部署和管理应用程序及类型,第三章:共享程序集和强命 ...

  3. CLR 的执行模型(2)

    第一章 CLR 的执行模型(2) 本篇内容大纲 Framework 类库(Framework Class Library , FCL) 通用类型系统(Common Type System,CTS) 公 ...

  4. 【C#进阶系列】01 CLR的执行模型——一个Hello World的故事

    好吧,废话少说,先上一章Hello World图: 我们有了一个Hello world程序,如此之简单,再加上我今天没有用汉字编程o(>﹏<)o,所以一切很简单明了. 故事开始: 编译: ...

  5. 第一部分 CLR基础:第1章 CLR的执行模型

    1.1将源代码编译成托管模块

  6. 第1章 CLR的执行模型

    1.1将源代码编译成托管代码模块

  7. 01.CLR的执行模型

    在非托管的C/C++中,可以进行一些底层的操作     "公共语言运行时"(CLR)是一个可由多种编程语言使用的"运行时"          CLR的核心功能包 ...

  8. CLR_Via_C#学习笔记之CLR的执行模型

    1:公共语言运行时(Common Language Runtime,CLR)是一个可由多种编程语言使用的“运行时”.CLR的核心功能(比如内存管理.程序集加载.安全性.异常处理和线程同步)可由面向CL ...

  9. [CLR via C#读后整理]-1.CLR的执行模型

    公共语言运行时(Common Language Runtime,CLR)是一个可由多种编程语言使用的"运行时".他主要提供的功能有:程序集加载,内存管理,,安全性,异常处理,线程同 ...

随机推荐

  1. 集合运算(UNION)

    表的加法 集合运算:就是满足统一规则的记录进行的加减等四则运算. 通过集合运算可以得到两张表中记录的集合或者公共记录的集合,又或者其中某张表中记录的集合. 集合运算符:用来进行集合的运算符. UNIO ...

  2. 团体程序设计天梯赛-练习集-L1-030. 一帮一

    L1-030. 一帮一 “一帮一学习小组”是中小学中常见的学习组织方式,老师把学习成绩靠前的学生跟学习成绩靠后的学生排在一组.本题就请你编写程序帮助老师自动完成这个分配工作,即在得到全班学生的排名后, ...

  3. Day7 字符串和常用数据结构

    字符串和常用数据结构 使用字符串 第二次世界大战促使了现代电子计算机的诞生,当初的想法很简单,就是用计算机来计算导弹的弹道,因此在计算机刚刚诞生的那个年代,计算机处理的信息主要是数值,而世界上的第一台 ...

  4. Hbuider sass配置 webstorm scss配置

    --no-cache %FileName% ../css/%FileBaseName%.css   sass编译后保存到css目录下 webstorm scss配置 C:\Ruby22-x64\bin ...

  5. jboss的下载安装、环境变量配置以及部署

    1. 下载安装 http://jbossas.jboss.org/downloads/   jdk为1.7 我下载的是:JBoss AS7.1.1.Final 2. 解压安装包  D:\program ...

  6. eas之控制kdtable滚动条

    //滚动条支持三种状态 自动 隐藏 显示 public static final int SCROLL_STATE_AUTO=0://自动根据数据判断是否显示或隐藏 public static fin ...

  7. grpc-web与react的集成

    很久没写总结了,在这里跟大家分享一下自己踩的坑,同时也方便自己多记忆下. 大致流程: 使用create-react-app脚手架生成react相关部分,脚手架内部会通过node自动起一个客户端,然后和 ...

  8. HDU1069 - Monkey and Banana【dp】

    题目大意 给定箱子种类数量n,及对应长宽高,每个箱子数量无限,求其能叠起来的最大高度是多少(上面箱子的长宽严格小于下面箱子) 思路 首先由于每种箱子有无穷个,而不仅可以横着放,还可以竖着放,歪着放.. ...

  9. Centos 修改主机名称

    Centos 配置主机名称: 1.首先查询一下当前的主机名称 [root@localhost~]# hostnamectl status Static hostname: ****** //永久主机名 ...

  10. keycode键盘 按键 - 键码 对应表

    字母和数字键的键码值(keyCode) 按键 键码 按键 键码 按键 键码 按键 键码 A 65 J 74 S 83 1 49 B 66 K 75 T 84 2 50 C 67 L 76 U 85 3 ...