浅析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         }

  在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(), 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     }

  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修饰的方法。

  

  

原文地址:https://www.cnblogs.com/liumengchen-boke/p/9123827.html

时间: 2024-10-12 09:10:06

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

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

.Net 从代码生成到执行,这中间的一些列过程是一个有别于其他的新技术新概念,那么这是一个什么样的过程呢,有什么样的机制呢,清楚了这些基本的东西我们做.Net的东西方可心中有数.那么,CLR的执行模型是一个什么样的过程呢? 将源代码编译成托管模块 --> 将托管模块合并成程序集 --> 加载公共语言运行时 --> 执行程序集的代码 目录 将源代码编译成托管模块 将托管模块合并成程序集 加载公共语言运行时 执行程序集的代码 本地代码生成器:NGen.exe Framwork类库入门 通用类

第一章 CLR的执行模型

概念篇 CLR(Common Language Runtime)[公共语言运行时] 可由多种编程语言使用的运行环境,提供内存管理.程序集加载.安全性.异常处理和线程同步等支持. CTS(Common Type System)[通用类型系统] 规范化的类型定义和管理,比如:字段.方法等, 又比如继承等特性. CLS(Common Language Specification)[公共语言规范] 针对CLR/CTS定义的最基本的组建. Manuged Module[托管模块] 标准的32位Mircor

01.CLR的执行模型

在非托管的C/C++中,可以进行一些底层的操作 "公共语言运行时"(CLR)是一个可由多种编程语言使用的"运行时" CLR的核心功能包含: 内存管理 程序集加载 安全性 异常处理 线程同步 可由支持CLR的编程语言编写源代码,再由对应的编译器检查语法和分析源代码,最后都会生成托管模块,托管模块需要CLR才能执行 托管模块的组成: PE32或PE32+头:包含与本地CPU代码相关信息 CLR头:托管模块的头信息,包含CLR版本,一些标志等 元数据:元数据表,包含源代码

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

好吧,废话少说,先上一章Hello World图: 我们有了一个Hello world程序,如此之简单,再加上我今天没有用汉字编程o(>﹏<)o,所以一切很简单明了. 故事开始: 编译: 一个程序写完肯定要编译,以前什么C啊什么的都是编译成本机的CPU指令,但是我们的C#不是. C#,VB.NET都会把它们编译成托管模块,托管模块在一个标准的可移植的PE文件中.(那些懵懂的少年肯定慌了,这是什么鬼,又是托管模块又是PE文件的.莫慌,所有你听不懂的高大上的术语其实都很简单,你现在不需要懂,听我慢

第一章、 CLR的执行模型

1. 概述 本章主要是介绍从源代码到可执行程序的过程中,CLR所做的工作. 2. 名词解释 ① 公共语言运行时(Common Language Runtime, CLR),是一个可由多种语言使用的 运行时,核心功能(内存管理.程序集加载.安全性.异常处理和线程同步)可由面向CLR的所有语言使用. ② PE32:32位 Microsoft Windows 可移植执行体.    PE32+:64位 Microsoft Windows 可移植执行体. ③ 元数据 是一组数据表,这些数据表描述了 模块中

第1章CLR的执行模型1.1

1.1将源代码编译成托管代码模块 如上图,用支持CLR的任何一种语言来创建源代码文件.再用一个对应的编译器来检查语法和分析源代码.经编译器编译后生成托管模块(managed module),它是一个可移植执行体文件,它可能是32位(PE32)文件,也可能是64位(PE32+)文件.托管模块包括中间语言和元数据,需要CLR才能执行. 公共语言运行时(Common Language Runtime, CLR)是一个供多种编程语言使用的运行时.可用任何编程语言来开发代码,只要编译器是面向CLR的就行.

浅析Java的内存模型

一.前言 ??之前过年在家,空闲时间比较多,想要了解一下JVM相关的内容,于是买了<深入理解Java虚拟机>这本书,过了一遍其中的基础知识.时隔多日,都忘得差不多了.为了重新捡起来,我决定复习一遍,并编写相关的系类博文加深印象,这是第一篇,来讲一讲JVM最基础的内容--Java的内存模型. 二.正文 ?2.1 Java内存分布 ??Java的内存主要分为五个部分: 程序计数器: Java虚拟机栈: 本地方法栈: 堆内存: 方法区: ??具体结构如下图所示: 2.2 程序计数器 ??首先看第一部

C#基础之CLR的执行模型(三)

三.加载公共语言运行时 可分为五步: 第一步,Windows检查EXE头文件,决定创建32位还是64位进程. 第二步,在进程中加载MSCorEE.dll的对应版本(x86.x64或ARM). 第三步,进程的主线程调用MSCorEE.dll的方法来初始化CLR. 第四步,CLR加载EXE程序集,再调用程序集的入口方法,然后,托管应用程序启动并运行. 第五步,咦--? 如果非托管应用程序调用LoadLibrary加载托管程序集,Windows会自动加载并初始化CLR,以处理程序集中的代码.由于进程已

01 CLR 的执行模型

1.先将不同语言的代码生成托管模块. 如图,每个语言有自己对应的编译器. 和普通编译器不同,这个编译器是生成IL代码,不是直接的机器码. 编译也就是我们平时生成的过程. f5 c# 调用 C:\Windows\Microsoft.NET\Framework64\v4.0.30319\csc.exe (我是这个版本,不同版本路径不同) 来编译. 也就是说.其实如果你不VS做开发, 可以只安装 .net Framework,写好代码之后通过它来编译 无视那个警告. 这就编译好了. 他可以是一个 DL