年前最后一篇技术博客了,由于近期的上线,自学进度严重滞后,年后还是得拾起来啊,闲言少叙,书归正传,经过艰苦的学习奋斗,终于来到了中级部分知识的最后一篇内容《异常处理》,其实之前学习Java的时候就了解过,在本质论系列的第五章也了解过,但是始终没有明确它的定位,它是干嘛的,什么时候用。综合日常的实战,我可以这么定义异常处理:异常处理通常和日志紧密配合,在可能出现问题的地方捕获系统抛出的异常然后打出对应的日志,方便系统稳定运行和程序员排查问题,搞明白了基础定位和作用之后,再来详细看看异常体系怎么运作。

多异常类型

程序不是抛出System.Exception,而是抛出更合适的ArgumentException。

抛出具体的异常类型

具体的异常类型本身指出什么地方出错(参数异常),并包含了特殊的参数来指出具体是哪一个参数出错

  public sealed class TextNumberParser{public static int Parse(string textDigit){string[] digitTexts = { "zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine" };#if !PRECSHARP7int result = Array.IndexOf(digitTexts,// Leveraging C# 2.0’s null coelesce operator(textDigit ??// Leveraging C# 7.0’s throw expressionthrow new ArgumentNullException(nameof(textDigit))).ToLower());#elseif(textDigit == null) throw new ArgumentNullException(nameof(textDigit))int result = Array.IndexOf(digitTexts, textDigit?.ToLower());
#endifif (result < 0){#if !PRECSHARP6throw new ArgumentException("The argument did not represent a digit", nameof(textDigit));
#elsethrow new ArgumentException("The argument did not represent a digit","textDigit");
#endif}return result;}}

以上部分的代码就是抛出了一个参数为null的处理方案,更具体的方式有:

  • ArgumentNullException和NullReferenceException这两个异常很相似。前者应在错误传递了空值时抛出。空值是无效参数的特例。非空的无效参数则应抛出ArgumentException或ArgumentOutOfRangeException。一般只有在底层“运行时”解引用null值(想调用对象的成员,但发现对象的值为空)时才抛出NullReferenceException
  • 开发人员不要自己抛出NullReferenceException。相反,应在访问参数前检查它们是否为空,为空就抛出ArgumentNullException,这样能提供更具体的上下文信息,比如参数名等。如果在实参为空时可以无害地继续,请一定使用C#6.0的空条件操作符(?.)来避免“运行时”抛出NullReferenceException。

也就是说,如果预期参数要求必须不为null否则程序无法进行下去了,则通过ArgumentNullException来给使用者抛出异常,如果为null程序也能继续,咱就别抛异常了,通过操作符来控制参数为null时候的代码走向,避免抛出不想要的如NullReferenceException的异常。总之就是看你想要的是什么了。

异常抛出规范

不要抛出System.Exception或System.ApplicationException这种过于宽泛的异常,这种异常没有任何有用信息,同时有几个直接或间接从System.SystemException派生的异常仅供“运行时”抛出,其中包括:

System.StackOverflowException【堆栈溢出】
System.OutOfMemoryException【内存溢出】
System.Runtime.InteropServices.COMException【服务器意外终止】
System.ExecutionEngineException【执行引擎异常】
System.Runtime.InteropServices.SEHException【服务器意外终止】

这些异常时万万不能自己抛出的,所以有些这样的规范:

  • 要在向成员传递了错误参数时抛出ArgumentException或者它的某个子类型。抛出尽可能具体的异常(例如ArgumentNullException),并且要在抛出ArgumentException或者它的某个子类时设置ParamName属性,便于定位问题参数
  • 要为传给参数异常类型的paramName实参使用nameof操作符。接收这种实参的异常类型包括ArgumentException,ArgumentOutOfRangeException和ArgumentNullException。
  • 要抛出最能说明问题的异常(最具体或者派生得最远的异常)。
  • 不要抛出NullReferenceException。相反,在值意外为空时抛出ArgumentNullException。
  • 不要抛出System.SystemException或者它的派生类型。
  • 不要抛出System.Exception或者System.ApplicationException。
  • 考虑在程序继续执行会变得不安全时调用System.Environment.FailFast()来终止进程

简单点总结就是,要抛出具体的能让你定位问题的异常,同时如果捕获了异常让系统再走下去也依然有问题,那么尽早终止就好了

新特性

以上的代码部分还有两个特性,一个是当参数名发生变更也能获取硬编码的nameof,一个是异常开始支持表达式类型

  • C#6.0开始支持使用nameof来获取变量的字面值,通过IDE修改参数,nameof也会打出最新的参数硬编码
  • C#7.0开始支持表达式异常,如果textDigit 为null则抛出异常,而在7.0之前必须使用if分支来进行判断:
result = Array.IndexOf( digitTexts, (textDigit ?? throw new ArgumentNullException(nameof(textDigit))));

捕捉异常

异常发生时,会跳转到与异常类型最匹配的catch块执行,匹配度由继承链决定,通常是继承链最远端来处理,例如,抛出的InvalidOperationException异常就会由 catch(InvalidOperationException exception)捕获。

   public sealed class Program{public static void Main(string[] args){try{throw new Win32Exception(42);//TextNumberParser.Parse("negative forty-two");// ...throw new InvalidOperationException("Arbitrary exception");// ...}//条件表达式catch(Win32Exception exception) when(args.Length == exception.NativeErrorCode){}catch(NullReferenceException exception){// Handle NullReferenceException}catch(ArgumentException exception){// Handle ArgumentException}catch(InvalidOperationException exception){// Handle ApplicationException}catch(Exception exception){// Handle Exception}finally{// Handle any cleanup code here as it runs// regardless of whether there is an exception}}}

有几点需要注意,也可以说是异常捕获的规范和新特性:

  • C#6.0起,catch块支持一个额外的条件表达式。不是只根据异常类型来匹配,现在可以添加when子句来提供一个Boolean表达式。条件为true,catch块才处理异常。当然可以在catch主体中用if语句执行条件检查。但这样做会造成该catch块先成为异常的“处理程序”,再执行条件检查,造成难以在条件不满足时改为使用一个不同的catch块。而使用异常条件表达式,就可先检查程序状态(包括异常),而不需要先捕捉再重新抛出异常。
  • 条件子句使用需谨慎: 如条件表达式本身抛出异常,新异常会被忽略,且条件被视为false。所以,要避免异常条件表达式抛出异常。
  • catch块必须按从最具体到最常规的顺序排列以避免编译错误。例如,将catch(Exception…)块移到其他任何一种异常之前都会造成编译错误。因为之前的所有异常都直接或间接从System.Exception派生。
  • catch块并非一定需要具名参数,例如catch(SystemException){…}。事实上,如下一节所述,最后一个catch甚至连类型参数都可以不要,这样的catch块叫做常规catch块
  • 在catch里使用throw来重新抛出异常而不替换堆栈信息,throw后边不能跟任何内容

还有包装异常这样的高级特性,不是很懂,暂时先不了解了,之后用的时候再学习下。

常规catch块

C#还支持常规catch块,即catch{},其行为和catch(System.Exception exception)块完全一致,只是没有类型名或变量名。此外,常规catch块必须是所有catch块的最后一个

 public static void Main(){try{// ...throw new InvalidOperationException("Arbitrary exception");// ...}catch (NullReferenceException ){// Handle NullReferenceException}catch (ArgumentException ){// Handle ArgumentException}catch (InvalidOperationException ){// Handle ApplicationException}catch (Exception ){// Handle Exception}// Disabled warning as this code is demonstrating the general catch block// following a catch(Exception ) block#pragma warning disable 1058catch{// Any unhandled exception}finally{// Handle any cleanup code here as it runs// regardless of whether there is an exception}}

感觉这个功能就是兼容C#1.0中不属于System.Exception的,因为从C#2.0开始才支持所有异常包装为派生自System.Exception,所以在1.0时代用常规catch块来覆盖全部异常情况,从2.0时代,常规catch块就和catch(System.Exception exception)的行为一致了。当然其实在原理上,catch块捕获的是object类型,也就是万类始祖,所以当然可以捕获任何异常喽:在CIL层面它的实质就是:catch(object).

异常处理规范

异常处理的一些最佳实践规范,这里的调用栈较高的位置就是上层方法,越高越面向用户,调用栈高的位置捕获调用栈低的位置的异常。:

  • 只捕捉能处理的异常。只应捕捉那些已知怎么处理的异常。其他异常类型应留给栈中较高的调用者去处理。
  • 不要隐藏(bury)不能完全处理的异常。catch(System.Exception)和常规catch块大多数时候应放在调用栈中较高的位置,除非在块的末尾重新抛出异常。
  • 尽量少用System.Exception和常规catch块
  • 避免在调用栈较低的位置报告或记录异常。这样记录下来的异常信息不全,而且用户也看不懂,继续向上抛就好了。而且抛的时候使用throw;
  • 在catch块中使用throw;而不是throw<异常对象>语句。只要不是重新抛出不同的异常类型,或者不是想故意隐藏原始调用栈,就应使用throw;语句,允许相同的异常在调用栈中向上传播
  • 想好异常条件来避免在catch块中重新抛出异常,如果发现会捕捉到不能恰当处理、所以需要重新抛出的异常,那么最好优化异常条件,从一开始就避免捕捉。
  • 避免在异常条件表达式中抛出异常,避免以后可能变化的异常条件表达式

以上就是主要规范,还有一个特殊规范体系是重新抛异常的规范:

重新抛出异常

重新抛出异常的时候有两种情况,一种就是同一种异常类型,要保留原始堆栈信息,咱就一直throw;另一种情况是捕获到栈低位置的异常后不想抛这种,想换一种那么更改异常类型,这个时候又要区分,是否要保留原始堆栈信息,如果要保留,就要使用包装异常,不保留就直接截断

public static void Main(string[] args){string url = "http://www.IntelliTect.com";if (args.Length > 0){url = args[0];}Console.Write(url);Task task = WriteWebRequestSizeAsync(url);try{while (!task.Wait(100)){Console.Write(".");}}catch (AggregateException exception){exception = exception.Flatten();ExceptionDispatchInfo.Capture(exception.InnerException).Throw();}}

这里的ExceptionDispatchInfo.Capture(exception.InnerException).Throw();就是抛出包装异常。

重新抛出不同异常时要小心:在catch块中重新抛出不同的异常,不仅会重置抛出点,还会隐藏原始异常。要保留原始异常,需设置新异常的InnerException属性(该属性通常可通过构造函数来赋值)。只有以下情况才可重新抛出不同的异常。

  • 更改异常类型可更好地澄清问题。例如,在对Logon(User user)的一个调用中,如遇到用户列表文件不能访问的情况,那么重新抛出一个不同的异常类型,要比传播System.IO.IOException更合适。
  • 私有数据是原始异常的一部分。在上例中,如文件路径包含在原始的System.IO.IOException中,就会暴露敏感的系统信息,所以应该使用其他异常类型来包装它(当然前提是原始异常没有设置InnerException属性
  • 异常类型过于具体,以至于调用者不能恰当地处理。例如,不要抛出数据库系统的专有异常,而应抛出一个较常规的异常,避免在调用栈较高的位置写数据库的专有代码。

只有以上这三种情况的时候才不用throw;语句而是throw Exception来进行操作

自定义异常设计规范

如果框架的异常类型不能满足自身需求,可以使用自定义异常,其全部继承自System.Exception,设计规范如下:

  • 所有异常都应该使用“Exception”后缀,彰显其用途。
  • 所有异常通常都应包含以下三个构造函数:无参构造函数、获取一个string参数的构造函数以及同时获取一个字符串和一个内部异常作为参数的构造函数。另外,由于异常通常在抛出它们的那个语句中构造,所以还应允许其他任何异常数据成为构造函数的一部分。

在继承体系上也需要注意,避免使用深的继承层次结构(一般应小于5级)。

本篇博客主要介绍了异常的多种类型,如何抛出,以及如何捕获,特别介绍了常规catch块、nameof、异常条件表达式的新特性,以及异常的一些使用规范,2019封箱之作,以飨诸位!

【C#本质论 十二】异常处理相关推荐

  1. spring boot 1.5.4 整合 mybatis(十二)

    上一篇:spring boot 1.5.4 整合log4j2(十一) Spring Boot集成Mybatis 更多更详细的配置参考文件:application.properties和<Spri ...

  2. php大量数据库抽象,PHP系列(十二)数据库抽象层pdo

    PHP系列(十二)数据库抽象层pdo 发布时间:2020-06-01 10:07:54 来源:51CTO 阅读:503 作者:sswqzx 1.数据库抽象层pdo (1).PDO(php data o ...

  3. (转)SpringMVC学习(十二)——SpringMVC中的拦截器

    http://blog.csdn.net/yerenyuan_pku/article/details/72567761 SpringMVC的处理器拦截器类似于Servlet开发中的过滤器Filter, ...

  4. 感谢十二年的陪伴——分享回归,不忘初心(Eastmount博客总结及未来规划)

    曾记否,2021年4月28日,为了更好地从事科研和学习,当时给所有读者群发了我在CSDN唯一的私信,感谢大家十年的陪伴,短暂消失,不负青春.当时也收到了很多博友的鼓励与祝福,感恩. 是啊!很难想象读博 ...

  5. Shiro第十二章-与Spring集成、配置文件初解

    简介 Shiro的组件都是Javabean/pojo式的组件,所以非常容易使用Spring进行组件管理,可以非常方便得从ini配置转为Spring配置(如xml配置文件). JavaSE 依赖 < ...

  6. 章节十二:编程思维:如何debug

    章节十二:编程思维:如何debug 目录 章节十二:编程思维:如何debug 1. bug 1:粗心 2. bug 2:知识不熟练 3. bug 3:思路不清 4. bug 4:被动掉坑 5. 习题练 ...

  7. 纯干货!Java后端开发十二条经验分享!

    前言 本文是博主从事后端开发以来,对公司.个人项目的经验总结,包含代码编写.功能推荐.第三方库使用及优雅配置等,希望大家看到都能有所收获 一. 优雅的进行线程池异常处理 在Java开发中,线程池的使用 ...

  8. 【哈工大软件构造】学习笔记10 第十章、第十一章、第十二章

    目录 第十章 面向可维护性的构造技术 1 软件维护和演化 2 可维护性的度量 3 模块化设计和模块性准则 模块划分的五个准则 模块设计的五个原则 耦合度和聚合度 4 OO设计准则:SOLID SRP ...

  9. 左耳听风 第四十二周

    左耳听风 第四十二周 每周完成一个ARTS: 每周至少做一个 leetcode 的算法题.阅读并点评至少一篇英文技术文章.学习至少一个技术技巧.分享一篇有观点和思考的技术文章.(也就是 Algorit ...

最新文章

  1. 实施Kubernetes可以实现多云架构安全
  2. Altium Designer 隐藏铺铜
  3. 【Android 性能优化】应用启动优化 ( 方法追踪代码模板 | 示例项目 | SD 卡访问权限 | 示例代码 | 获取 Trace 文件 | Android Studio 查看文件)
  4. matlab 连续显示,请教下MATLAB一个问题啊 我想检测一行数据里面出现连续出现0的次数,...
  5. 指针津逮--------浅谈从指针到“ref”
  6. Cloudera Manager 和CDH6.0.1安装,卸载,各步骤截图(此博文为笔者辛苦劳作最终生成的,使用了3个熬到凌晨2~4点的夜晚,外加一个周末完成,请转载时记录转载之处,谢谢)
  7. iris数据_MAT之ELM:ELM实现鸢尾花(iris数据集)种类测试集预测识别正确率(better)结果对比...
  8. MySQL数据库序列的作用_MySQL数据库:序列使用
  9. 前端学习(3257):react中添加todolist
  10. Exchange Server 2016管理系列课件46.DAG管理之Powershell创建DAG
  11. C# BackgroudWorker
  12. 新颖的自我介绍_公众场合,如何做一个吸睛的自我介绍?
  13. 【NOI2004】【洛谷P1486】郁闷的出纳员(Splay写法)
  14. 大型web工程的session管理器构想
  15. paip.提升稳定性---c3p0数据库连接池不能取到连接An attempt by a client to checkout a Connection has timed out
  16. 在Ubuntu上用wine下载QQ,三种打开QQ的方法
  17. Docotic.Pdf New Crack by Crackdung
  18. wind 债券数据 python_【Python学习】使用Wind接口分析同业存单的到期数据
  19. react ssr 简单实现
  20. 毕业之后才知道的——知网查重原理以及降重举例

热门文章

  1. 通过编程模拟一个简单的饮料自动贩卖机_你喝过自动贩卖的现榨橙汁吗?
  2. “卷王”英伟达的真面目
  3. 月入8000+的steam搬砖项目,你应该知道(详细教程)
  4. DeprecationWarning: There is no current event loop loop = asyncio.get_event_loop()
  5. xshell更改终端目录颜色
  6. 可以500%提高开发效率的前端UI框架
  7. 微信小程序倒计时,购物车,向左滑删除 左拉删除
  8. 软件研发部经理岗位职责
  9. 龙芯3a5000下安装部署rabbitmq
  10. 干货 | 阻抗与导纳控制:一种使机器人刚中带柔的控制方法