发布于 

dotNET:怎样处理程序中的异常(理论篇)?

平时在软件开发的过程中,首先是要保证功能可以正常运行,满足业务需求,除此之外,还需要考虑代码在异常的时候怎么处理,让程序能够健壮地运行。正确合理地处理异常可以减少程序的 Bug、保证代码质量,当然也不是一件很容易的事。

在日常工作中我们排查错误时经常会遇到这样一些问题,如果没有,说明你做的还不错了:

  • 想通过日志的方式分析错误原因,发现日志记录不完整;
  • 找到错误日志了,记录的是“未将对象引用设置到对象的实例”,也知道代码行数,然而这一行上有多个引用类型的对象,还是不知道真实原因;
  • 问题是偶发的,无法重现。

最终需要还原数据库进行单步调试才能解决问题,然而:

  • 客户的数据库涉密,不能提供;
  • 客户的数据库运行多年,数据量很大,无法快速备份还原;
  • 如果是互联网 Saas 应用,更是难于将库拿到本地进行调试。

所以需要在代码层面、在日志层面来进行优化来达到可以快速定位问题的目的。

dotNET 经典错误

上面这张图,经历过 dotNET Framework 时代的程序员应该都不陌生,这就是经典的「黄页」和经典的 「未将对象引用设置到对象的实例」错误。

首先这个错误显示非常不友好,除了让人知道这个是 dotNET 开发的,别无他用,另外这个错误提示对排查错误也没有帮助,只知道对象为 null 了,但原因是什么并不知道,只能猜,能不能猜中就得看运气了。

正确的错误处理思路

一个系统一般有两类人使用,普通用户和系统管理员。不管是普通用户还是系统管理员,在操作系统时都期望所有的操作是有反馈的,要么正常返回想要的结果,要么给出友好的错误提示,能够指引进行下一步操作。

当出现异常时,可以导向一个专属类型的错误提示页面,也可以以模态的方式弹出错误提示,内容包含:

  • 错误提示,例如:系统异常,请联系管理员,拨打 xxx 、保存失败,请联系管理员;
  • 全局错误码,下面会讲到;
  • 异常编码,可以根据此编码在后台的日志记录快速查询,异常编码使用日期加流水号即可,建议不要使用 Guid,曾经被非技术人员当成是乱码。

如果是系统管理员使用的功能,将真实错误原因显示在错误提示中,我认为也是可以的。

全局错误码

设置全局错误码,可以让管理员在收到反馈的错误时能快速地根据错误码进行问题的定位和找到解决方法。所以需要有公开的全局错误码文档,记录错误的原因和解决方案参考。

大类上可以分为 4xx 和 5xx,4xx 表示前端的参数问题、验证问题等,5xx 表示后端的逻辑问题。

在 5xx 类型中可以再进行细分,例如:

  • 500100:表示数据库操作相关问题
  • 500200:表示列表展示相关问题
  • 等等

异常处理的一些原则

1、在方法中不要返回错误码,因为错误码的信息太单一;
2、抛异常时选择具体的异常类型,不要直接抛出 System.Exception ;
3、错误信息目的是为了让开发人员可以定位问题和解决问题,而不是给最终用户看,给前端用户看的信息要友好易懂;
4、不能吞异常,比如 catch 异常后不做任何处理,如果有些资源需要清理,可以使用 try…finally 或者使用 using ;
5、只有当你知道怎么样从异常中恢复时,才需要去捕获异常,在执行一些操作时,我们可能知道出现错误的原因,但无法恢复,这时不要去捕获异常。

在方法中怎样处理异常?

一个方法中有三个部分:参数、业务逻辑和返回值

参数

引用类型的参数,在方法的开始一定要做非空判断,判断后是抛异常还是继续下面的逻辑这个要根据具体情况来定:

  • 如果参数为 null 时会对后续的业务有影响,就应该抛出异常;
  • 如果我们判断 null 后能做一些初始化处理,能让程序继续正常运行,而且保证业务也是正确的,就不必抛异常。

业务逻辑

业务逻辑的部分分为三种情况:

  • 在方法内部调用其他类型的一个方法,比如 var user= userService.GetUser(); 对 user 的判断,当为 null 时是否抛异常,跟上面参数的逻辑一致;
  • 多个逻辑组合到一起进行判断后,如果不能满足下一步的输入,应该抛出异常;
  • 对于更低一层的调用,有时会进行异常的捕获,当捕获到异常后,应该要抛出符合当前上下文的专有异常信息,更利于定位问题。

返回值

一个方法的返回值可以返回值类型,如 string、int、bool ,也可以返回引用类型,如返回一个 User 对象,不管是返回什么类型,原则是一样的,都需要更具上下文来进行判断。

有个 GetUser 方法来获取用户对象 ,如果根据 Id 没有找到用户,可以直接返回 null ,而不是返回一个空的 User 对象,如果返回空对象,程序不会出错,但前端展示却没有数据,就搞不清是没找到用户,还是找到了但没值;返回 null,可以由上层来决定怎么来处理。

再有个 GetUserList 方法根据条件获取用户集合,如果根据搜索条件没有找到符合的用户,可以返回空对象 List ,而不是返回 null 。

对于值类型也是一样,要看上下文,比如 C# 中用来查找字符在一个字符串中的索引位置的函数 IndexOf ,返回的是 int 类型,当找不到的时候返回的是 -1 ,而不是 null 。

最后

好的异常处理可以使我们的程序更加的健壮,也能在出现问题时更好的定位和排查问题,本文的内容偏理论,下一篇以代码示例的方式来进行演练下。

希望本文对您有所帮助。