【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) 那在上篇文章,我们讲了, ...
随机推荐
- Senparc.Weixin.MP SDK 微信公众平台开发教程(十六):AccessToken自动管理机制
在<Senparc.Weixin.MP SDK 微信公众平台开发教程(八):通用接口说明>中,我介绍了获取AccessToken(通用接口)的方法. 在实际的开发过程中,所有的高级接口都需 ...
- webservice 之 WSDL的解析
先看一个wsdl, <?xml version="1.0" encoding="UTF-8" standalone="no"?> ...
- UISwitch
UISwitch *noticeSwtich = [[UISwitch alloc] initWithFrame:CGRectMake(0, 0, 51, 31)]; // noticeSwtich. ...
- Hibernate中对象的三个状态解析
Hibernate 将操作的对象分为三种状态: 1. 瞬时 (Transient )/临时状态/自由状态 持久 (Persistent) 脱管 (Detached) 瞬时对象特征: 第一.不处于 Se ...
- Java-接口练习
编写2个接口:InterfaceA和InterfaceB:在接口InterfaceA中有个方法voidprintCapitalLetter():在接口InterfaceB中有个方法void print ...
- [Reomting Debug] 巧用VS 的remote debug 功能远程调试程序 经验分享.
前言: 有时候我们Dev(开发人员)需要debug tester(测试人员)或者customer(客户)的环境,可tester的机器上没有Code,是不是有点着急? 而且是多版本应用且tester 发 ...
- Liferay7 BPM门户开发之44: 集成Activiti展示流程列表
处理依赖关系 集成Activiti之前,必须搞清楚其中的依赖关系,才能在Gradle里进行配置. 依赖关系: 例如,其中activiti-engine依赖于activiti-bpmn-converte ...
- JavaScript的陷阱
这本来是翻译Estelle Weyl的<15 JavaScript Gotchas>,里面介绍的都是在JavaScript编程实践中平时容易出错或需要注意的地方,并提供避开这些陷阱的方法, ...
- OpenCascade Shape Representation in OpenSceneGraph
OpenCascade Shape Representation in OpenSceneGraph eryar@163.com 摘要Abstract:本文通过程序实例,将OpenCascade中的拓 ...
- Cocos2d-x 3.2 学习笔记(十五)保卫萝卜 场景与数据
保卫萝卜~场景的思路以及数据的存储. 学习要写笔记,记录自己的步骤. 一.场景构建Tiled 关于Tiled网上有一大堆的教程,这个比较好用,特别是构建塔防类的游戏极其简 ...