- 本博客所总结书籍为《CLR via C#(第4版)》清华大学出版社,2021年11月第11次印刷(如果是旧版书籍或者pdf可能会出现书页对不上的情况)
- 你可以理解为本博客为该书的精简子集,给正在学习中的人提供一个“glance”,以及对于部分专业术语或知识点给出解释/博客链接。
- 【本博客有如下定义“Px x”,第一个代表书中的页数,第二个代表大致内容从本页第几段开始。(如果有last x代表倒数第几段,last代表最后一段)】
- 电子书可以在博客首页的文档-资源归档中找到,或者点击:传送门自行查找。如有能力请支持正版。(很推荐放在竖屏上阅读本电子书,这多是一件美事)
- 欢迎加群学习交流:637959304 进群密码:(CSGO的拆包密码)
目录
- 第二十章 异常和状态管理
- 定义“异常”
- 异常处理机制
- System.Exception类
- FCL定义的异常类
- 抛出异常
- 定义自己的异常类
- 用可靠性换取开发效率
- 设计规范和最佳实践
- 未处理的异常
- 对异常进行调试
- 异常处理的性能问题
- 约束执行区域(CER)
- 代码协定
第二十章 异常和状态管理
定义“异常”
- 书中举了几个异常的例子,例如传参null值等(P400 1)
异常处理机制
- C#一异常处理机制的标准用法:try、catch、finally(P401 last)
private void sormeMethod()
{
try
{
//需要得体地进行恢复和/或清理的代码放在这里
}
catch (Invalid0perationException)
{
//从Invalid0perationException恢复的代码放在这里)
}
catch (IOException)
{
//从IOException恢复的代码放在这里
}
catch
{
//从除了上述异常之外的其他所有异常恢复的代码放在这里
//如果什么异常都捕捉,通常要重新抛出异常。本章稍后将详细解释
throw ;
}
finally
{
//这里的代码对始于try块的任何操作进行清理
//这里的代码总是执行,不管是不是抛出了异常
}
//如果try块没有抛出异常,或者某个catch 块捕捉到异常,但没有抛出或
//重新抛出异常,就执行下面的代码
}
- try:执行一般性的资源清理操作,需要从异常中恢复,或者是可能抛出异常的代码。(P402 2)
- catch:相应一个异常需要执行的代码,一个try可以匹配多个catch,执行时会自上而下进行检测。如果没有异常则不执行catch而直接执行finally块,如果没有finally块则直接执行后续的代码。(P402 3) catch末尾的三个选择:1、重新抛出相同的异常,向调用栈高一层的代码通知该异常的发生。2、抛出一个不同的异常,向调用栈高一层的代码提供更丰富的异常信息。3、让线程从catch块的底部退出。(P403 4)
- finally:一个try最多只能关联一个finally块,处理异常的语句应当放在finally块中,否则可能会因为异常抛出而没有捕捉到,就无法执行后续语句。(P404 3)
System.Exception类
- C#有System.Exception类型,并规定C#只允许抛出派生自System.Exception的异常。(P407 1)
- 一般在Windows应用程序事件日志或崩溃转储(crash dump)中查看,而非直接访问异常属性。
有System.Exception类型的公共属性
- throw抛出异常。一个异常抛出时,CLR在内部记录throw指令的位置(抛出位置)。一个catch 块捕捉到该异常时,CLR记录捕捉位置。在 catch 块内访问被抛出的异常对象的StackTrace属性,负责实现该属性的代码会调用CLR内部的代码,后者创建一个字符串来指出从异常抛出位置到异常捕捉位置的所有方法。(P408 2)CLR只记录最新的异常对象的抛出位置。
FCL定义的异常类
- FCL定义的异常类,都是从System.Exception类型派生,书中列举了所有的异常。(P410 1)
抛出异常
- 如在Exception中有符合相关意思的异常可以直接派生后抛出异常,如果没有则自定义类型再从System.Exception中派生。(P412 3)第二点,要根据异常向异常类型的构造器传递符合的字符串消息。
定义自己的异常类
- 构造自己的异常类需要允许序列化,以便于穿越AppDomain边界或者写入日志/数据库。以及书中的代码示例(P413 1)
用可靠性换取开发效率
- 编译器能隐式地做下面的事情:1、调用方法时插入可选数值 2、对值类型的实例进行装箱 3、构造/初始化参数数组 4、绑定到dynamic变量/表达式的成员 5、绑定到扩展方法。 6、绑定/调用重载的操作符(方法)。构造委托对象。 7、在调用泛型方法、声明局部变量和使用lambda表达式时推断类型。为lambda表达式和迭代器定义/构造闭包类。 8、定义/构造/初始化匿名类型及其实例。 9、重写代码来支持LIlNQ查询表达式和表达式树。(P415 last)
- CLR隐式做如下事情:1、调用虚方法和接口方法。 2、加载程序集并对方法进行JIT编译,可能抛出以下异常:FileLoadException ,BadImageFormatException , InvalidProgramException ,FieldAccessException ,MethodAccessException , MissingFieldException , MissingMethodException和VerificationException。 3、访问MarshalByRefObject派生类型的对象时穿越AppDomain边界(可能抛出AppDomainUnloadedException)。 4、穿越AppDomain边界时序列化和反序列化对象。 5、调用Thread.Abort或AppDomain.Unload时造成线程抛出ThreadAbortException。垃圾回收之后,在回收对象的内存之前调用 Finalize方法。 6、使用泛型类型时,在 Loader堆中创建类型对象3。 7、调用类型的静态构造器”R(可能抛出TypeInitializationException)。8、抛出各种异常,包括OutOfMemoryException,DividleByZeroException,NullReferenceExceptionRuntimeWrappedException , TargetInvocationException , OverflowException ,NotFiniteNumberException,ArrayTypeMismatchException,DataMisalignedExceptionIndexOutOfRangeException,InvalidCastException,RankException,SecurityException等(P416 1)
- 在C#和CLR进行隐式任务执行时,以及代码的多样组合性可能会造成无法对于错误进行准确的判断和定位。
- 如何在缓解对代码状态的破坏情况下,进行错误的捕捉:(P420 1) 1、执行catch或 finally块中的代码时,CLR不允许线程终止。所以,可以把一些状态更改操作放到finally块中进行。 2、可以使用System.Diagnostics.Constracts.Constract类向方法应用代码协定。 3、可以使用约束执行区域(Constrained Execution Region,CER),他能消除CLR的某些不确定性。 4、根据状态的存在位置,利用事务(transaction)来确保状态要么都修改,要么都不修改。 5、将自己的方法设计得更明确。
- 如果状态已经损坏到无法修复的程度,应立即销毁所有损坏的状态,以防止更多的破坏。托管的状态泄漏不到AppDomain外部,所以销毁AppDomain的状态可以直接调用AppDomain的Unload方法卸载。(第22章会讲到)(P422 last)
设计规范和最佳实践
- 善用finally块(P423 last),确保清理代码的执行: 1、使用lock 语句时,锁在finally块中释放。 2、使用using 语句时,在finally 块中调用对象的 Dispose方法。 3、使用foreach 语句时,在. finally块中调用IEnumerator对象的 Dispose方法。 4、定义析构器方法时,在 finally块中调用基类的 Finalize方法。
- 不要过于频繁或不恰当地使用catch块。(P424 last3)
- 如果能准确判断到可能会出现的异常,那么可以写一些代码允许应用程序从异常中得体地恢复并继续执行。(P425 last)
- 发生不可恢复的异常时回滚部分完成的操作–维持状态。为了正确回滚已部分完成的操作,代码应捕捉所有异常。在C#中只需要单独使用throw关键字,不在throw后指定任何内容。(P246 last)
- 隐藏实现细节来维系协定。有时需要捕捉一个异常并重新抛出不同的异常。这样做唯一的原因是维系方法的“协定”(contract)。另外,抛出的新异常类型应该是一个具体异常(不能是其他异常类型的基类)。(P427 3)
未处理的异常
- 异常抛出时,CLR在调用栈中向上查找与抛出的异常对象的类型匹配的catch 块。没有任何catch 块匹配抛出的异常类型,就发生一个未处理的异常。CLR检测到进程中的任何线程有未处理的异常都会终止进程。(P429 last2)
- 示例(P430)
对异常进行调试
- 在调试菜单栏中打开异常显示界面。(P434)
异常处理的性能问题
- 异常处理的代价:1、非托管C 编译器必须生成代码来跟踪哪些对象被成功构造。编译器还必须生成代码,以便在一个异常被捕捉到的时候,调用每个已成功构造的对象的析构器。由编译器担负这个责任是很好的,但会在应用程序中生成大量簿记(bookkeeping)代码,对代码的大小和执行时间造成负面影响。 2、另一方面,托管编译器就要轻松得多,因为托管对象在托管堆中分配,而托管堆受垃圾回收器的监视。如对象成功构造,而且抛出了异常,垃圾回收器最终会释放对象的内存。编译器无需生成任何簿记代码来跟踪成功构造的对象,也无需保证析构器的调用。与非托管C 相比,这意味着编译器生成的代码更少,运行时要执行的代码更少,应用程序的性能更好。(P436 2)
约束执行区域(CER)
- 由于AppDomain可能被卸载,造成它的状态被销毁,所以一般用CER处理由多个AppDomain或进程共享的状态。如果要在抛出了非预期的异常时维护状态,CER就非常有用。有时将这些异常称为异步异常。例如,调用方法时,CLR必须加载一个程序集,在AppDomain的 Loader堆中创建类型对象,调用类型的静态构造器,并将IL代码JIT编译成本机代码。所有这些操作都可能失败,CLR通过抛出异常来报告失败。(P438 3)
代码协定
- 代码协定(code contract)提供了直接在代码中声明代码设计决策的一种方式。这些协定采取以下形式: 1、前条件:一般用于对实参进行验证。 2、后条件:方法因为一次普通的返回或者抛出异常而终止时,对状态进行验证。 3、对象不变性(Object Invariant):在对象整个生命周期内,确保对象的字段的良好状态。
public static class contract
{
//前条件方法: [ conditional ("CONTRACTS_FULL")]
public static void Requires (Boolean condition) ;
public static void EndContractBlock () ;
//前条件:Always
public static void Requires<1Exoeption>(Boolean condition)where TExoeption : Exoeption;
//后条件方法: [conditional ("CONTRACTS_FULL")]
public static void Ensures (Boolean condition ) ;
public static void EnsuresonThrow<TException>(Boolean condition)
where TException : Exception;
//特殊后条件方法: Always
public static T Result<T> () ;
public static T oldvalue<T>(T value) ;
public static T valueAtReturn<T>(out T value) ;
//对象不变性方法: [ conditional ("CONTRACTS_FULL") ]
public static void Invariant (Boolean condition) ;
//限定符(Quantifier)方法:Always
public static Boolean Exists<T>(IEnumerable<T> collection,Predicate<T> predicate);
public static Boolean Exists(Int32 fromInclusive,Int32 toExclusive,Predicate<Int32> predicate) ;
public static Boolean ForAll<T>(IEnumerable<T> collection,Predicate<T> predicate);
public static Boolean ForAll(Int32 fromInclusive,Int32 toExclusive,Predicate<Int32> predicate) ;
//辅助(Helper)方法[Conditional ("CONTRACTS_FULL")]或[Conditional ("DEBUG")]
public static void Assert (Boolean condition) ;
public static void Assume (Boolean condition) ;
//基础结构(Infrastructure)事件:你的代码一般不使用这个事件
public static event EventHandler<ContractFailedEventArgs> ContractFailed;
}