【C#进阶系列】20 异常和状态管理
异常就是指成员没有完成它的名称所宣示的行动。
public class Girl {
public string Name { get; set; }
}
public class Troy{
Girl girl;
public void Love() {
Console.WriteLine("Troy爱上了" + girl.Name);
}
}
上面这段代码会有异常,因为Troy去执行Love这个函数,然而其中girl根本就没有赋值。本来Troy预期完成爱一个姑娘这个行动,结果发生了异常的事情,姑娘离开了Troy。
异常要解决的问题
很多行为(比如方法和属性)很多时候都没法返回错误代码(比如void方法,构造器,属性的获取设置),但他们仍然需要报告错误,于是异常就来解决这个问题。
也就是说异常处理机制实际上是为了返回可预知的错误代码,而不是为了去捕获未知的异常让程序不报错。(这一点非常重要)
不要去让程序吞异常,不把异常暴露出来让其继续运行,反而可能使程序做出更错误的举动。(有错就改,别藏着)
那么其实我在刚学习的时候一直有个疑问,我这个系统很多人在用啊,你如果不吞异常,那报黄页不是更6?
现在我认为这并不矛盾,如果有异常就在catch后进行异常处理还原操作,然后写日志或者用一个统一的页面去提示用户出错了,而不是把黄页去给用户看。(就像你告诉别人你得胃病了,用嘴和肢体语言表述都行,你剖开自己的肚子告诉别人你有病就是你的错了啊)
.Net的异常处理机制
.Net的异常处理机制是基于windows提供的结构化异常处理机制(Structured Exception Handing,简称SEH)构建的。
异常处理的代码就不演示了,说说三大块
- try块
- 一个try块中如果能抛出同一个异常类的操作,却要进行不同的异常恢复措施,那么应该分成两个try块。
- try和finally到一起一般是执行资源清理操作(也可以用using哦)。
- catch块
- 一个try块可以关联0个或多个catch块。
- catch后面跟着圆括号中的表达式称为捕捉类型,异常捕捉类型必须是System.Exception或者它的派生类。
- CLR自上而下搜索异常,所以要将较具体的异常放在顶部。也就是说首先写派生程度最大的异常,然后才是其基类,然后才是System.Exception或者不指定任何捕捉类型的catch块。
- 如果抛出的异常没有catch到,也就是说catch的类型没有一个与抛出的异常匹配,那么CLR就回去调用栈更高的一层搜索与异常匹配的捕捉类型。如果到了调用栈的顶部还是没有匹配到catch块,就会发生未处理的异常。而一旦找到匹配的catch块,就会执行内层所有finally块的代码,否则内层所有finally块的代码都不会执行。也就是说下面示例代码中会报异常:
static void Main(string[] args)
{
try
{
FuncA();
}
finally {
Console.WriteLine("主函数Finally");
} Console.Read();
} static void FuncA() {
try
{
Object obj = new DateTime();
int a = (int)obj;//这里会报System.InvalidCastException异常
}
catch(InvalidDataException)//表示不匹配,然后到调用栈的上一级也就是main函数,然而main函数中的try根本就没有catch所以更谈不上什么匹配,也就是出现了一个未处理的异常
{
//这里完全不会执行
}
finally {//虽然有Finally说好的,不论是否异常都会执行,然而此时上面的异常没有catch到,
//所以已经异常报错了,不会再执行到这里。此时CLR会终止进程,相较于让程序继续运行造成不可预知的结果这样更好
Console.WriteLine("函数A的Finally");
}
} - catch块的末尾有以下三种处理方法:
- 重新抛出相同的异常,向调用栈高一层的代码通知该异常的发生,也就是throw;
- 抛出一个不同的异常,向调用栈高一层的代码提供更丰富的异常信息,也就是throw ex;//这里ex为新的异常对象
- 让线程从catch块底部退出,不向更高层抛异常。
- 代码可向AppDomain的FirstChanceException事件登记,这样只要AppDomain一发生异常就会收到通知,并且在CLR开始搜索任何catch块之前就会调用这些事件回调函数。
- finally块
- finally块为保证会执行的代码。
- 如果在catch内部和finally内部又抛出了异常,那么在try中的异常不会被记录,其信息将丢失。
System.Exception类
微软规定所有CLS相容的编程语言都必须抛出和捕捉派生自该类型的异常。
一般来讲也就这个类中也就三个属性要注意:
- Message指出抛出异常的原因
- InnerException如果当前异常是在处理一个异常时抛出的,那么InnerException中就是上一个异常。用公共方法GetBaseException可以遍历内部异常链表,返回最初抛出的异常。
- StackTrace包含异常抛出前调用过的所有方法的名称和签名。它返回一个从异常抛出位置到异常捕捉未知的所有方法。
抛出异常
抛异常需要考虑两个问题:
第一个是抛出什么Exception类型的异常。应该选择一个更有意义的类型。要考虑到调用栈中高处的代码,要知道那些代码如何判断一个方法失败从而执行得体的恢复代码。作者强烈建议异常的继承层次结构应该浅而宽,这样就可以尽量少的创建基类。而基类意味着把众多错误当做一个错误来处理。
第二个是向异常类型的构造器传递什么字符串消息。
自定义异常类
看起来自定义异常类很简单,只需要继承System.Exception类就OK了,然而实际上这是个很繁琐的事情。
因为从System.Exception类派生出来的所有类都应该是可序列化的,使它们能穿越AppDomain边界去写入日志或者数据库。而序列化就涉及到很多问题。
作者写了个泛型异常类去简化,我这里就不写了,实际上在格式上找个系统异常照着写就行了:

别忘了在自定义类上面加上[Serializable]特性。
作者的玩法更高端一点,自己建个泛型异常类继承Exception,然后将一些构造函数或者序列化函数写在这个类中。个性化的异常信息作为泛型变量T传给泛型异常类来使用,以此起到简化作用。
设计规范和最佳实践
- 不要什么都捕捉
- 就像前面说的捕捉异常表明已经预见到了此异常,理解它为什么发生,并知道如何处理它。如果catch了System.Exception就表明你确定预知到了一切异常,并且知道如何处理,仿若神明。
- 所以应该有针对性地捕捉异常,而不是吞噬异常,没有捕捉到的异常请抛出。(有一种有趣的玩法就是用一个线程去吞噬异常然后给出结果,然后另一个线程去检测结果然后重新抛出该异常)
- 发生不可恢复的异常时回滚部分完成的操作——维护状态
- 捕捉到异常后看能否写代码简单回滚,不行的话也可以用事务来处理。
- 隐藏实现细节来维系协定
- 如果需要传递给上层更多的信息,可以直接在异常的Data属性中添加信息
- 可以尝试着用对用户而言更形象的异常去包装实际发生的异常然后抛出,但是必须将实际发生的异常作为这个更形象的异常的InnerException。
未处理的异常
未处理的异常就是指那些未catch到的异常(调用栈向上查找也没catch到)。
应用程序应建立处理未处理异常的策略,而微软建议开发人员接收CLR的默认策略。也就是说,应用程序发生未处理异常时,程序终止,windows会向事件日志写一条记录。
可通过事件查看器查看该记录:

还可用可靠性监视程序查看应用程序的更多细节

图上显示我的dota2在3月25号又崩了,点后面可以查看详细信息。
我们可以将未处理的异常自己去写日志记录下来,或者发邮件什么的都行。而微软的每种应用程序模型都有自己的与未处理异常打交道的方式。
然而对于服务端程序而言,发生了未处理的异常,理想情况下是记录日志,然后向客户端发送通知,表明请求无法完成,最终终止服务器应用程序。(这个太扯了,作者也说太理想了)
对于服务器应用程序,与未处理异常有关的信息不应该返回客户端,首先用户这些信息用户并不能解决,其次服务器应该尽量少暴露自己的相关信息,防止被黑。(这个必须保证)
异常处理的性能问题
异常的处理实际上性能并不好,有的人用返回true和false,或者搞个类返回这个类的实例对象,此对象中既包含方法是否成功,又包含错误信息,以此来处理异常。(从需求上来看,这两种解决方法是等价的,也就是说异常机制和这种玩法的目的是一样的)。
然而作者并不推荐这种方法,因为要同时处理CLR和类库的抛出异常和自己的代码的返回错误代码。(这个理由其实有点牵强)
异常处理和返回错误码的方式相比,很难看出两者在性能上的差异。(虽然这么说,但是其实还是有影响的。有些人提倡直接返回布尔值和错误信息,然而实际上因为代码毕竟是人写的,往往很难得到彻底落实,也往往会导致一些人catch所有异常再返回true和false,不仅吞异常性能也没提高)
然而对于托管代码而言,因为托管对象在托管堆中分配,而托管堆受垃圾回收器的监视。如对象成功构造,而且抛出异常,垃圾回收器最终会释放掉对象的内存。编译器无需像非托管代码那样生成任何bookkeeping代码来跟踪成功构造的对象,也无需保证析构器的调用。因为垃圾处理器已经帮你自动处理了。在这一点上与非托管的C++相比,编译器会生成更少的代码,运行时执行的代码更少,性能也会更好。
事实上无论多么小的性能影响在过于频繁后都会产生不小的性能影响,比如如果一个int.Parse方法,用户经常输入无法解析的数据,那么如果去catch异常,那么因为过去频繁抛出和捕捉异常,必定会对应用程序的总体性能产生很大影响。
为了解决这类问题,微软为Int32类提供了TryParse方法。此方法转换字符串然后以out引用变量去输出结果,返回值为Boolean类型表示转换是否成功。
应确保一般情况下的方法不会失败,如果频繁抛异常导致性能不好才应该考虑添加一些tryXXX之类的方法。
约束执行区域(CER)
根据定义CER必须是对错误有适应力的代码块。由于AppDomain可能被卸载,造成状态被销毁,所以一般用CER处理多个AppDomain或进程共享的状态。如果要在抛出了非预期的异常时维护状态,CER就很有用。有时将这些异常称为异步异常。
class Program
{
static void Main(string[] args)
{
//执行以下代码,将代码体指定为受约束的执行区域即CER
RuntimeHelpers.PrepareConstrainedRegions();////System.Runtime.CompilerServices命名空间
try
{
Console.WriteLine("哈哈");
}
finally {
//隐式调用Troy的构造方法,此时如果类型静态构造函数发生异常,会在执行RuntimeHelpers.PrepareConstrainedRegions()的时候就抛出
//因为从名字就可以看出RuntimeHelpers.PrepareConstrainedRegions()就是对后面的异常机制中catch和finally的代码JIT进行提前编译。(前提是这些方法应用了ReliabilityContract)
Troy.Show();
}
} }
public class Troy {
static Troy() {
Console.WriteLine("类型构造函数被调用");
}
//应用在System.Runtime.ConstrainedExecution命名空间中定义的这个特性
[ReliabilityContract(Consistency.WillNotCorruptState,Cer.Success)]
public static void Show() { }
}
RuntimeHelpers的另一个方法ExecuteCodeWithGuaranteedCleanup,它在资源保证得到清理的前提下才执行代码。
代码协定
代码协定提供了直接在代码中声明代码设计决策的一种方式。
采用以下形式:
- 前条件
- 一般用于对实参进行验证
- 后条件
- 方法因为一次普通的返回或者抛出异常而终止时,对状态进行验证
- 对象不变性
- 在整个对象生命期内,确保对象的字段的良好状态
代码协定有利于代码的使用、理解、进化、测试、文档和早期错误检测。
代码协定的核心类为静态类System.Diagnostics.Constracts.Constract.
【C#进阶系列】20 异常和状态管理的更多相关文章
- 对CLR异常和状态管理的一点理解
一:自己的感悟 今天读到<CLR via C#>的异常和状态管理这一章,作者给出了关于异常处理的诸多建议,里面有一些建议自己深有体会,比如说使用可靠性换取开发效率这一节.之前自己对异常怎么 ...
- 读书笔记—CLR via C#异常和状态管理
前言 这本书这几年零零散散读过两三遍了,作为经典书籍,应该重复读反复读,既然我现在开始写博了,我也准备把以前觉得经典的好书重读细读一遍,并且将笔记整理到博客中,好记性不如烂笔头,同时也在写的过程中也可 ...
- [CLR via C#]异常和状态管理
当CLR检测到某个正在运行的.NET应用程序处于一种特殊的正常执行顺序被打断的状态时,会生成一个异常对象来表示这个错误,并将此对象在方法调用堆栈中向上传送.如果一个程序引发了一个异常却没有处理,CLR ...
- C#进阶系列 ---- 《CLR via C#》
[C#进阶系列]30 学习总结 [C#进阶系列]29 混合线程同步构造 [C#进阶系列]28 基元线程同步构造 [C#进阶系列]27 I/O限制的异步操作 [C#进阶系列]26 计算限制的异步操作 ...
- ASP.NET MVC+EF框架+EasyUI实现权限管理系列(20)-多条件模糊查询和回收站还原的实现
原文:ASP.NET MVC+EF框架+EasyUI实现权限管理系列(20)-多条件模糊查询和回收站还原的实现 ASP.NET MVC+EF框架+EasyUI实现权限管系列 (开篇) (1):框架 ...
- vue从入门到进阶:Vuex状态管理(十)
Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式.它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化. 在 Vue 之后引入 vuex 会进行自动 ...
- Ext JS学习第十六天 事件机制event(一) DotNet进阶系列(持续更新) 第一节:.Net版基于WebSocket的聊天室样例 第十五节:深入理解async和await的作用及各种适用场景和用法 第十五节:深入理解async和await的作用及各种适用场景和用法 前端自动化准备和详细配置(NVM、NPM/CNPM、NodeJs、NRM、WebPack、Gulp/Grunt、G
code&monkey Ext JS学习第十六天 事件机制event(一) 此文用来记录学习笔记: 休息了好几天,从今天开始继续保持更新,鞭策自己学习 今天我们来说一说什么是事件,对于事件 ...
- 当我们说线程安全时,到底在说什么——Java进阶系列(二)
原创文章,同步发自作者个人博客,转载请以超链接形式在文章开头处注明出处http://www.jasongj.com/java/thread_safe/ 多线程编程中的三个核心概念 原子性 这一点,跟数 ...
- 简简单单的Vue3(插件开发,路由系统,状态管理)
既然选择了远方,便只顾风雨兼程 __ HANS许 系列:零基础搭建前后端分离项目 系列:零基础搭建前后端分离项目 插件 路由(vue-router) 状态管理模式(Vuex) 那在上篇文章,我们讲了, ...
随机推荐
- 实战使用Axure设计App,使用WebStorm开发(3) – 构建页面架构
系列文章 实战使用Axure设计App,使用WebStorm开发(1) – 用Axure描述需求 实战使用Axure设计App,使用WebStorm开发(2) – 创建 Ionic 项目 实战使 ...
- Linux网路编程系列-网络I/O模型
应用程序从网络中拿数据,要经历两个阶段:1.等待数据准备好-分组到达,被拷贝到内核缓冲区,组装数据报:2.数据从内核缓冲区拷贝至用户态应用程序的缓冲区.Unix下五个I/O模型: 阻塞I/O: 进程调 ...
- Java连接Oracle数据库开发银行管理系统【一、需求篇】
此系统开发共分为三篇完成. 第一篇[需求篇]:效果展示图,也就是需求部分的展示 第二篇[设计篇]:需求分析和类,接口的设计 第三篇[实现篇]:具体代码实现
- EF架构~CodeFirst数据迁移与防数据库删除
回到目录 本文介绍两个概念,防数据库自动删除,这是由于在code first模式下,当数据实体发生变化时,会对原来数据库进行删除,并将新数据表添加进来,但这对于我们的运营环境数据库,是万万不能接受的, ...
- PHP面向对象之魔术方法复习
魔术方法复习 2014-9-2 10:08:00 NotePad++ By jiancaigege 飞鸿影~========================= 1.__construct() 构造方法 ...
- Atitit cnchar simp best list 汉字简化方案 最简化汉字256个
Atitit cnchar simp best list 汉字简化方案 最简化汉字256个 1.1. 最简化发音1 1.2. 根据笔画密度,删除了密度高的字..1 1.3. 使用同发音的英文字母等代 ...
- SDN跟网络虚拟化的完美结合
SDN跟网络虚拟化的完美结合 之前说过,所谓的“SDN最适合的领域是数据中心”的说法,笔者认为更准确的说法应该是SDN最适合的领域是数据中心中的网络虚拟化应用.为什么说SDN 非常适合用在网络虚拟化中 ...
- cordova platform add specified version
cordova platform add specified version 命令格式 cordova platform add android@4.0 可用的版本 Valid install tar ...
- 在忘记root密码的情况下如何修改linux系统的root密码
1.系统启动时长按shift键后可以看到如下界面: 2.找到 recovery mode 那一行, 按下[e]键进入命令编辑状态,到 linux /boot/vmlinuz-....... r ...
- PHP的学习--PHP的引用
引用是什么 在 PHP 中引用意味着用不同的名字访问同一个变量内容.这并不像 C 的指针,替代的是,引用是符号表别名.注意在 PHP 中,变量名和变量内容是不一样的,因此同样的内容可以有不同的名字.最 ...