文章目录:

  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代码。 如下如所示:

         private static void Exfun1()
{
try
{
Exfun2();
}
catch(Exception ex)
{
Console.WriteLine($" this is Exfun1 Exception : {ex.StackTrace}"); //3
}
finally
{
Console.WriteLine("this is Exfun1 finally"); //
}
}
private static void Exfun2()
{
try
{
Console.WriteLine("this is Exfun2"); //1
throw new IOException();
}
catch(InvalidCastException ex)
{
Console.WriteLine($"this is Exfun2 InvalidCastException {ex.Message}"); //由于捕获的异常与抛出的异常不匹配,所以不执行
}
finally
{
Console.WriteLine("this is Exfun2 finally"); //2 由于是在Exfun1中的catch捕获到异常,所以先执行内层的catch块。
}
}

  在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重置异常起点。例如:

        private static void SomeMehtod()
{
try
{
Console.WriteLine("this is someMthod1");
SomeMethod2();
}
catch (Exception e)
{
Console.WriteLine($"method1 reset exception line {e.StackTrace}");
}
}
private static void SomeMethod2()
{
try
{
Console.WriteLine("this is someMthod2");
throw new IOException();
}
catch (IOException e)
{
Console.WriteLine($"method2 exception line {e.StackTrace}");
throw e;
}
}

异常位置重置

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

         private void SomeMethodNoReset()
{
bool trySucceeds = false;
try
{
//dosomething
trySucceeds = true;
}
finally
{
if (!trySucceeds)
{ }
}
}

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

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

FirstChanceException

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

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

 private static void Go()
{
LoadAssemblies();
var allTypes = (from a in AppDomain.CurrentDomain.GetAssemblies()
from t in a.ExportedTypes
where typeof(Exception).GetTypeInfo().IsAssignableFrom(t.GetTypeInfo())
orderby t.Name
select t).ToArray();
Console.WriteLine(WalkInherirtanceHierarchy(new StringBuilder(), , typeof(Exception), allTypes));
}
private static StringBuilder WalkInherirtanceHierarchy(StringBuilder sb, int indent, Type baseType, IEnumerable<Type> allTypes)
{
string spaces = new string(' ', indent * );
sb.AppendLine(spaces + baseType.FullName);
foreach (var t in allTypes)
{
if (t.GetTypeInfo().BaseType != baseType) continue;
WalkInherirtanceHierarchy(sb, indent + , t, allTypes);
}
return sb;
}
private static void LoadAssemblies()
{
String[] assemblies = {
"System, PublicKeyToken={0}",
"System.Core, PublicKeyToken={0}",
"System.Data, PublicKeyToken={0}",
"System.Design, PublicKeyToken={1}",
"System.DirectoryServices, PublicKeyToken={1}",
"System.Drawing, PublicKeyToken={1}",
"System.Drawing.Design, PublicKeyToken={1}",
"System.Management, PublicKeyToken={1}",
"System.Messaging, PublicKeyToken={1}",
"System.Runtime.Remoting, PublicKeyToken={0}",
"System.Runtime.Serialization, PublicKeyToken={0}",
"System.Security, PublicKeyToken={1}",
"System.ServiceModel, PublicKeyToken={0}",
"System.ServiceProcess, PublicKeyToken={1}",
"System.Web, PublicKeyToken={1}",
"System.Web.RegularExpressions, PublicKeyToken={1}",
"System.Web.Services, PublicKeyToken={1}",
"System.Xml, PublicKeyToken={0}",
"System.Xml.Linq, PublicKeyToken={0}",
"Microsoft.CSharp, PublicKeyToken={1}",
}; const String EcmaPublicKeyToken = "b77a5c561934e089";
const String MSPublicKeyToken = "b03f5f7f11d50a3a"; Version version = typeof(System.Object).Assembly.GetName().Version; foreach (String a in assemblies)
{
String AssemblyIdentity =
String.Format(a, EcmaPublicKeyToken, MSPublicKeyToken) +
", Culture=neutral, Version=" + version;
Assembly.Load(AssemblyIdentity);
}
}

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. 隐藏实现细节来维系协定;例如现在有一个获取用户电话号码的功能,通过输入名字,从文件中找到匹配号码并返回。假如文件不存在或者文件读取异常,这时候就不应该将这两个异常信息返回给用户,应该返回一个自定义的用户尚未找到该用户的号码这样的异常给调用者。 以下是伪代码:
     public sealed class PhoneBook
    {
    private string m_pathname; //地址簿文件路径名称
    public string GetPhoneNumber(string name)
    {
    string phone;
    FileStream fileStream = null;
    try
    {
    //根据name从fs中读取内容
    fileStream = new FileStream(m_pathname, FileMode.Open);
    byte[] bt = new byte[];
    fileStream.Read(bt, , );
    phone = System.Text.Encoding.Default.GetString(bt);
    return phone;
    }
    catch(FileNotFoundException ex)
    {
    //重新抛出一个不同的异常,而且加入name
    //将原来的异常设置为内部异常
    throw new NameNotFoundException(name, ex);
    }
    catch(IOException ex)
    {
    throw new NameNotFoundException(name, ex);
    }
    }
    }
    public class NameNotFoundException : Exception {
    public NameNotFoundException(string name,Exception e) { }
    }
  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. 关于OpenCV的Mat画图问题

    由于OpenCV的java版本画图有太多错误,只能自己编写画图的代码,在一个函数中,编写出画圆和深度距离的代码, 代码如下: public int CircleMyMat(Mat Show, Poin ...

  2. 读书笔记「Python编程:从入门到实践」_9.类

    9.1 创建和使用类 面向对象编程——Object Oriented Programming,简称OOP,是一种程序设计思想. OOP把对象作为程序的基本单元,一个对象包含了数据和操作数据的函数. 把 ...

  3. [实战经验][SQL Sever 2008 (R)解决方法累积

    SQL Sever 2008 (R)的安装图解及配置 http://www.soft6.com/v9/2009/jcsj_1030/115821.html 产品密钥,选择“输入产品密钥”,输入:PTT ...

  4. 新浪某个tab 页模仿

    <!doctype html> <html> <head> <meta charset="utf-8"> <title> ...

  5. js通过插件发送邮件

    这个插件为SmtpJS 官网地址为  https://www.smtpjs.com/ 方法很简单 <script src="https://smtpjs.com/v2/smtp.js& ...

  6. hadoop spark 总结

    yarn  由,资源管理器rm,应用管理器am appMaster,节点管理器nm 组成! 图侵删 yarn 的设计,是为了代替hadoop 1.x的jobtracker 集中式一对多的资源管理「资源 ...

  7. BZOJ 1984月下“毛景树” LCT维护边权 + 下传标记

    Description 毛毛虫经过及时的变形,最终逃过的一劫,离开了菜妈的菜园. 毛毛虫经过千山万水,历尽千辛万苦,最后来到了小小的绍兴一中的校园里.爬啊爬~爬啊爬~~毛毛虫爬到了一颗小小的“毛景树” ...

  8. 基于MATLAB的语音信号处理

    一.图形界面设计 1.新建GUI界面 2.新建空白页 3.命名为"yydsp",打开界面 4.拖放控件 5.按预定功能修改界面 6.填写Callback函数 未填写前的代码: fu ...

  9. 15.3 Task Task.Yield和Task.Delay说明

    https://blog.csdn.net/hurrycxd/article/details/79827958 书上看到一个Task.Yield例子,Task.Yield方法创建一个立即返回的awai ...

  10. Context、Select(day01)

    Oracle sql: 4天 plsql: 2天 proc: 2天 数据库介绍 1.1 数据库简介 1.1.1 数据管理技术的发展 人工管理阶段:20世纪50年代中期之前 文件管理阶段:20世纪的50 ...