.NET 提供了一种统一的方式来报告应用程序的错误, 即通过引发异常来指示具体问题. 这相比于 Win32 时代的错误处理 (通过 GetLastError 或者 HRESULT 的方式), 不但要简单明了得多, 还更容易维护. 通过监控程序可能引发的异常, 并对异常做出相应的处理 (比如数据恢复, 日志记录), 可以提高程序的可靠性, 可维护性
对异常的理解
异常, 即程序在运行过程中, 遇到的错误或意外的行为
在 .NET 中, 异常都是从 System.Exception 类继承. 具体的异常由发生问题的代码引发, 然后它在堆栈中向上传递, 直到应用程序对其进行处理或者程序终止为止
Exception 定义如下
- public class Exception : ISerializable, _Exception {
- public Exception();
- public Exception(string message);
- public virtual string Source { get; set; }
- public virtual string HelpLink { get; set; }
- // 包含可用于确定错误位置的堆栈跟踪
- // 如果有可用的调试信息, 则堆栈跟踪包含源文件名和程序行号
- public virtual string StackTrace { get; }
- public MethodBase TargetSite { get; }
- public Exception InnerException { get; }
- // 一般情况, 我们可以通过这个属性, 来理解具体的异常
- public virtual string Message { get; }
- public int HResult { get; protected set; }
- public virtual IDictionary Data { get; }
- }
常见的异常有
- IndexOutOfRangeException
- ,
- NullReferenceException
- ,
- ArgumentNullException
等等.
每一种异常, 都对应于一个特定的情形. 在实际项目中, 如果需要自定义异常, 也应该遵循这个原则. 比如
IndexOutOfRangeException
针对的就是数组或集合访问越界的情形
使用 try...catch 块捕获异常
对于有可能产生异常的代码, 我们可以使用 try...catch 将这些代码包围起来, 并在 catch 子句中指明需要捕获的异常, 同时可以在 catch 的代码块内, 对该异常做出相应的处理
一般情况下, 我们应该在 catch 子句中指明具体的异常, 因为这样我们就能很方便地对不同的异常做出具体的处理, 以便程序尽可能的从异常中恢复过来. 比如像下面这样
- try {
- using (StreamReader sr = File.OpenText("data.txt")) {
- Console.WriteLine($"First Line: {sr.ReadLine()}");
- }
- } catch (UnauthorizedAccessException e) {
- // 针对未授权做出处理. 比如请求管理员权限
- } catch (DirectoryNotFoundException e) {
- // 针对目录不存在处理. 比如弹窗提示用户
- } catch (FileNotFoundException e) {
- // 针对文件不存在处理. 比如弹窗处理
- }
当然, 如果我们仅仅是为了捕获异常, 并记录日志. 此时我们只需要在 catch 子句中, 使用 Exception 即可 (Exception 类为所有异常类的基类), 如下
- try {
- using (StreamReader sr = File.OpenText("data.txt")) {
- Console.WriteLine($"First Line: {sr.ReadLine()}");
- }
- } catch (Exception e) {
- // 这种情况下, 我们可以通过日志记录的模块, 来记录此次异常
- // 以便后期维护使用
- }
如果我们在处理了特定的异常之后, 希望将其他意料之外的异常也记录在日志中
需要 特别注意的是: Exception 只能放置在最后的 catch 子句中, 因为它是其他异常的基类
示例如下
- try {
- using (StreamReader sr = File.OpenText("data.txt")) {
- Console.WriteLine($"First Line: {sr.ReadLine()}");
- }
- } catch (UnauthorizedAccessException e) {
- // 针对未授权做出处理. 比如请求管理员权限
- } catch (DirectoryNotFoundException e) {
- // 针对目录不存在处理. 比如弹窗提示用户
- } catch (FileNotFoundException e) {
- // 针对文件不存在处理. 比如弹窗处理
- } catch(Exception e) {
- // 通过日志记录的模块, 记录意料之外的异常
- }
因此, 有以下结论:
在 catch 子句中, 子类应该放在其父类的前面. 否则, 将无法捕获具体子类的异常. 比如前面的这段代码, 如果我们将 Exception e 放置在其他的异常之前, 那么其他异常的具体处理逻辑都无法执行
我们应该如何引发异常
我们可以使用 throw 语句显式引发异常. 使用方式有两种
方式一
throw 异常对象的方式. 比如, 我们可以通过
throw new ArgumentNullException()
来引发一个参数为空的异常; 也可以使用
throw new Exception()
来引发, 不过 不建议这样使用. 使用特定情形的异常对象是最好的做法
使用方式如下
- public void ProcessData(int[] source, int from, int count) {
- if (source == null)
- throw new ArgumentNullException("source", "The source you provided cannot be null");
- if (from <0 || from>= source.Length)
- throw new IndexOutOfRangeException("The'from'parameter is out of range");
- // ...
- // 其他逻辑
- // ...
- }
这种引发异常的方式, 在我们的写公共的类库的时候会经常用到. 因为在公共类库中, 我们需要将发生问题的详细信息传递出去, 以方便使用类库的开发者调试
方式二
直接使用 throw; 的方式, 这种使用方式只能存在于 catch 子句中. 当我们在处理了具体的异常之后, 仍然希望上层能够捕获此异常的时候非常有用
使用方式如下
- try {
- // 业务逻辑代码
- } catch (UnauthorizedAccessException e) {
- // 异常处理逻辑
- // 将异常传递出去
- throw;
- }
创建自定义异常
在预定义的异常不符合业务需求的情况下 (比如预定义异常无法携带我们需要的信息时), 我们可以通过从 Exception 类派生来创建自己的异常类
比如, 我们需要一个数据库中用户不存在的异常, 则可以按如下方式处理
- public class UserNotExistException : Exception {
- public string UserName { get; }
- public string UserId { get; }
- public UserNotExistException(string userName, string userId) {
- this.UserName = userName;
- this.UserId = userId;
- }
- }
在用户不存在的情况下, 通过引发此
UserNotExistException
异常, 我们可以很容易的获取到不存在的用户的 ID 及昵称
使用 finally 块
定义在 finally 块中的代码, 其表示: 无论 try 块中是否有异常发生, 都会执行. 常见于资源的清理, 比如文件操作, 网络操作或数据库操作完成之后
如下代码所示
- StreamReader sr = null;
- try {
- sr = File.OpenText("data.txt");
- Console.WriteLine($"First Line: {sr.ReadLine()}");
- } catch (UnauthorizedAccessException e) {
- } catch (DirectoryNotFoundException e) {
- } catch (FileNotFoundException e) {
- } catch (Exception e) {
- } finally {
- // 无论前面是否发生异常, 我们都需要销毁文件资源
- if (sr != null) {
- sr.Dispose();
- }
- }
COM 互操作异常
一般情况下, 如果因 COM 方法失败而返回 HRESULT, 运行时会将其映射为可由托管代码捕获的异常. 例如, E_ACCESSDENIED 将映射为
UnauthorizedAccessException
,E_OUTOFMEMORY 映射为
OutOfMemoryException
, 等等
如果 HRESULT 为自定义值, 或 CLR 无法将其映射成预定义的具体托管异常. 运行时会引发 COMException 异常, 其 ErrorCode 属性包含具体的 HRESULT 值
编码建议
设计良好的异常处理机制可以防止应用崩溃. 这部分介绍了在实际项目中处理和创建异常的一些建议
合理使用 try...catch, 过于频繁的使用, 会造成性能低下 (如果某些异常一直出现). 况且, 我们也不应该过于依赖异常处理机制, 对业务逻辑中具体情况进行良好的处理, 比用 try...catch 更有意义. 比如, 当我们尝试关闭已关闭的连接时, 就会引发
InvalidOperationException
异常. 为了避免这个异常, 我们可以在尝试关闭前, 通过使用 if 语句检查连接状态, 避免该情况
对于某些情况, 如果在返回 null, 或者类型的默认值的情况下, 不会影响对方法的理解. 那么我们应该返回类型的默认值或 null, 而不是去引发一个异常
如果可以不引发异常, 那么就不引发异常. 这时我们只需要在程序中对特定的情况进行处理修正即可. 一般情况下, 如果引发异常无法带来好处, 或者并没有让我们提供的接口, 方法等更易于理解, 那就没必要引发异常
仅在异常需要携带某些自定义数据的情况下, 去自定义异常 (该异常类应该以 Exception 结尾). 否则, 我们使用系统预定义异常即可
在每个异常中都包含一个本地化描述字符串. 一般情况下, 如果不是跨国籍合作, 我们可以都使用中文, 或者一律使用英文, 也可以混搭, 这个根据公司项目的情况而定
在实际项目中, 我们应该参考上面的建议, 以帮助我们写出性能和可维护性都较好的代码
至此, 这篇文章的内容讲解完毕. 欢迎关注公众号 [嘿嘿的学习日记] , 所有的文章, 都会在公众号首发, Thank you~
来源: https://juejin.im/post/5b3b650d6fb9a04f8f377872