浅析CLR的异常处理模型
文章目录:
- 异常概述
- CLR中的异常处理机制
- CLR中异常的核心类System.Exception类
- 异常处理的设计规范和最佳实践
- 异常处理的性能问题
- 其他拓展
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、异常处理的设计规范和最佳实践
- 善用finally块,在执行catch块和finally块中的代码的时候,CLR不允许线程终止。所以,finally块中代码始终会执行,应该先用finally块清理那些已经成功启动的操作,再返回至调用者或者执行finally块之后的代码;利用finally块中代码显示释放对象避免资源泄露。
- 例如使用lock语句,锁将在finally块中被释放。
- 使用using语句时候,finally块中调用对象的Dispose方法。
- foreach语句,再finally方法中调用IEnumerator对象的Dispose方法。
- 析构方法,在finally块中调用基类的Finalize方法。
- 不要什么都捕捉,不要过于频繁的,不恰当的使用catch块。不要把异常吞噬掉,而是应该允许一场在调用栈中向上移动,让应用程序代码针对性处理。
- 得体的从异常中恢复。
- 发生不可恢复的异常时,回滚部分完成的操作来维持状态。
- 例如要序列化一组对象到磁盘文件,当中途失败时,要文件回滚到对象序列化之前的状态。
- 隐藏实现细节来维系协定;例如现在有一个获取用户电话号码的功能,通过输入名字,从文件中找到匹配号码并返回。假如文件不存在或者文件读取异常,这时候就不应该将这两个异常信息返回给用户,应该返回一个自定义的用户尚未找到该用户的号码这样的异常给调用者。 以下是伪代码:
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) { }
} - 对于未处理的异常会造成进程终止,这些异常可以在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的异常处理模型的更多相关文章
- 01.由浅入深学习.NET CLR 基础系列之CLR 的执行模型
.Net 从代码生成到执行,这中间的一些列过程是一个有别于其他的新技术新概念,那么这是一个什么样的过程呢,有什么样的机制呢,清楚了这些基本的东西我们做.Net的东西方可心中有数.那么,CLR的执行模型 ...
- 第一章 CLR 的执行模型
CLR via C# 读书笔记:第一章 CLR 的执行模型(1) 第Ⅰ部分CLR基础.这部分为三章(第一章:CLR的只想能够模型,第二章:生成.打包.部署和管理应用程序及类型,第三章:共享程序集和强命 ...
- CLR 的执行模型(2)
第一章 CLR 的执行模型(2) 本篇内容大纲 Framework 类库(Framework Class Library , FCL) 通用类型系统(Common Type System,CTS) 公 ...
- 【C#进阶系列】01 CLR的执行模型——一个Hello World的故事
好吧,废话少说,先上一章Hello World图: 我们有了一个Hello world程序,如此之简单,再加上我今天没有用汉字编程o(>﹏<)o,所以一切很简单明了. 故事开始: 编译: ...
- 第一部分 CLR基础:第1章 CLR的执行模型
1.1将源代码编译成托管模块
- 第1章 CLR的执行模型
1.1将源代码编译成托管代码模块
- 01.CLR的执行模型
在非托管的C/C++中,可以进行一些底层的操作 "公共语言运行时"(CLR)是一个可由多种编程语言使用的"运行时" CLR的核心功能包 ...
- CLR_Via_C#学习笔记之CLR的执行模型
1:公共语言运行时(Common Language Runtime,CLR)是一个可由多种编程语言使用的“运行时”.CLR的核心功能(比如内存管理.程序集加载.安全性.异常处理和线程同步)可由面向CL ...
- [CLR via C#读后整理]-1.CLR的执行模型
公共语言运行时(Common Language Runtime,CLR)是一个可由多种编程语言使用的"运行时".他主要提供的功能有:程序集加载,内存管理,,安全性,异常处理,线程同 ...
随机推荐
- spring中的prop、set、list、map
props.set.list.map这些事spring配置文件中很常见的标签,下面说下各自的适用场合. props:用于键值对,建和值都为string类型. <property name=&qu ...
- 防止split没有切割的变量报错
var getSocketUrl = localStorage.getItem("socketUrl"); getSocketUrl = getSocketUrl &&am ...
- 当svn检出项目检出一半时停止,如何继续检出
1.当svn检出项目时,发现中断,又不想重新检出可以在已检出的项目目录下右键 2.然后点击 之后直接update你的项目就可以了
- Emacs学习笔记之主模式笔记
% 模式相关—————————————————————————— 这里经常用的也就各种语言相对应的模式,有些就没再写进去,比如f90等模式 Fundamental mode 基本模式 ...
- WPF通过鼠标滑轮缩放显示图片
如果你使用WinForm比较难实现通过滚动鼠标滑轮来对图片进行缩放显示,那么,你应该考虑一下使用WPF,既然是下一代Windows客户端开发平台,明显是有一定优势的,不然,MS是吃饱了撑着. 首先 ...
- python第十二周:SQL alchemy、pymysql
python操作MySQL mysqldb python3之后的版本就不支持mysqldb了,故在此略过 pymysql #执行SQL语句 # -*- coding:utf-8 -*- #!/user ...
- String Boot-thymeleaf使用(四)
简介 Thymeleaf是面向Web和独立环境的现代服务器端Java模板引擎,能够处理HTML,XML,JavaScript,CSS甚至纯文本.,可以完全替代jsp,也是spring boot官方推荐 ...
- mysql 的load data infile
LOAD DATA INFILE语句从一个文本文件中以很高的速度读入一个表中.如果指定LOCAL关键词,从客户主机读文件.如果LOCAL没指定,文件必须位于服务器上.(LOCAL在MySQL3.22. ...
- codeforces 257c
#include<stdio.h> int main() { __int64 n,m,k,i,j,a,b; while(scanf("%I64d%I64d%I64d", ...
- Wireshark中的一些SNMP相关的过滤器
Wireshark中的一些SNMP相关的过滤器 转自 http://linmingren2003.blog.163.com/blog/static/567510032011419825097/ 由 ...