应该抛出什么异常?不应该抛出什么异常?(.NET/C#)

应该抛出什么异常?不应该抛出什么异常?(.NET/C#)

2018-02-04 13:25

我在 .NET/C# 建议的异常处理原则 中描述了如何 catch 异常以及重新 throw。然而何时应该 throw 异常,以及应该 throw 什么异常呢?


究竟是谁错了?

代码中从上到下从里到外都是在执行一个个的包含某种目的的代码,我们将其称之为“任务”。当需要完成某项任务时,任务的完成情况只有两种结果:

  1. 成功完成
  2. 失败

异常处理机制就是处理上面的第 2 种情况。这里我们不谈论错误码系统,那么,异常便应该在任务执行失败时抛出异常。

抛出异常后,报告错误只是手段,真正要做的是帮助开发者修复错误。于是,第一个要做的就是区分到底——谁错了!

  • 任务的使用者用错了
  • 任务的执行代码写错了
  • 任务执行时所在的环境不符合预期

简单说来,就是:使用错误,实现错误、环境错误。

让我们把异常归类到这些错误中

本文的重点在于指导我们何时应该抛出什么异常,也就是说——我们的角色是——任务的编写者。那么,编写者有责任编写出一段没有错误的代码。这就说明——永远不应该抛出表示自己写错了的异常

那么,我们对常见的异常进行分类。

使用错误

  • ArgumentException 表示参数使用错了
    • ArgumentNullException 表示参数不应该传入 null
    • ArgumentOutOfRangeException 表示参数中的序号超出了范围
    • InvalidEnumArgumentException 表示参数中的枚举值不正确
  • InvalidOperationException 表示当前状态下不允许进行此操作(也就是说存在着允许进行此操作的另一种状态)
    • ObjectDisposedException 表示对象已经 Dispose 过了,不能再使用了
  • NotSupportedException 表示不支持进行此操作(这是在说不要再试图对这种类型的对象调用此方法了,不支持)
    • PlatformNotSupportedException 表示在此平台下不支持(如果程序跨平台的话)

实现错误

  • NullReferenceException 试图在空引用上执行某些方法,除了告诉实现者出现了意料之外的 null 之外,没有什么其它价值了
  • IndexOutOfRangeException 使用索引的时候超出了边界
  • InvalidCastException 表示试图对某个类型进行强转但类型不匹配
  • StackOverflow 表示栈溢出,这通常说明实现代码的时候写了不正确的显式或隐式的递归
  • OutOfMemoryException 表示托管堆中已无法分出期望的内存空间,或程序已经没有更多内存可用了
  • AccessViolationException 这说明使用非托管内存时发生了错误
  • BadImageFormatException 这说明了加载的 dll 并不是期望中的托管 dll
  • TypeLoadException 表示类型初始化的时候发生了错误

环境错误

  • IOException 下的各种子类
  • Win32Exception 下的各种子类
  • ……

无法归类

不应该抛出,却又不得不抛出的异常:

  • NotImplementedException 这只能说明此功能还在开发中,一旦进入正式环境,不要抛出此异常(如果那时真的没有完成,这个方法就应该删除)
  • AggregateException 如果可能,真的不要抛出此异常,因为它本身不包含异常信息,让使用者很难正确 catch 这样的异常。如果内部只有一个异常,应该使用 ExceptionDispatchInfo 将内部异常合并(请参阅 使用 ExceptionDispatchInfo 捕捉并重新抛出异常 - 吕毅)(Task 在执行多个任务后,如果多个任务都发生了异常,就抛出了 AggregateException,但这已经是没有办法的事情了,因为没有办法将两个可能不是同类的异常合并成一个)

永远都不应该抛出异常:

  • FormatException 这算是 .NET 设计上的失误吧……因为当它抛出来时无法准确描述到底什么错了
  • ApplicationException 这是各种异常的基类,本身并没有明确的意义
  • SystemException 这是各种异常的基类,本身并没有明确的意义
  • Exception 这可是顶级基类,这都抛出来了,使用者再也无法正确地处理此异常了

是时候该决定抛什么异常了

对于使用错误,应该在第一时间抛出

既然对方已经用错了,那么代码继续执行也只会错上加错。

public string Foo(Bar demo)
{
    demo.Output("Walterlv");
    return _anotherDemo.ToString();
}

例如上面的方法中使用者传入了一个 null 参数后,方法必然执行失败 —— 抛出了一个 NullReferenceException。但是,当拿着这样的异常去调查哪里错了的时候,我们会发现 demoanotherDemo 都可能为 null。

然而很明显,这时使用者的错,使用者确保传入的参数不为 null,方法就可以继续执行。

如果在方法的一开始就抛出使用异常 ArgumentNullException,那么就可以向使用者报告这样的参数使用错误。

另外的情况,_anotherDemo 是此类型中的另一个字段,此时也要求必须非 null。而要确保非 null,使用者必须使用其它方式隐式初始化这个字段,那么应该抛出 InvalidOperationException,告诉使用者应该先调用其他的某个方法。

那么,应该改成:

public string Foo([NotNull] Bar demo)
{
    if (demo == null)
        throw new ArgumentNullException(nameof(demo));
    if (_anotherDemo == null)
        throw new InvalidOperationException("必须使用 XXX 设置某个值之后才能使用 Foo 方法。");

    demo.Output("Walterlv");
    return _anotherDemo.ToString();
}

当然,不像 ArgumentNullExceptionInvalidOperationException 通常并不一定能在开始就确定是否满足状态要求,但最好能尽可能在第一时间抛出,避免错误蔓延。

做到了第一时间抛出使用错误,就能让使用者明确知道自己用错了,需要修改使用代码。(这正是被另外一项事实所逼——典型的程序员是不看文档的,“使用异常”代替了一部分文档。)

永远不应该让实现错误抛出

这一节的标题其实说了三件事情:

  1. 永远不应该主动用 throw 句式抛出“实现错误”章节中提到的任何异常
  2. 如果你在调用某个别人实现的代码时遇到了“实现错误”章节中提到的异常,那说明“那个人”的代码写出 BUG 了,确信无疑。
  3. 如果自己写的代码发现抛出了这些异常,那就说明自己写出了 BUG,需要第一时解决 BUG(是解决,不是逃避)

我们假设实现了这段代码:

var button = (Button) sender;
button.Content = "Clicked";

如果在执行到第一句时发生了 InvalidCastException,说明实现代码编写是不正确的。

为了防止发生异常,可能有些人会改成这样:

// 请注意:这段示例是错误的!
if (sender is Button button)
    button.Content = "Clicked";

这是在逃避问题,而不是解决问题!

这是一段典型的事件处理函数代码,sender 通常是事件的引发者。写这段代码的人并没有调查 sender 不是 Button 类型的原因,到底是因为在 Grid 上监听了路由事件的 Click,还是因为多个控件都把事件处理函数设为了这个方法。如果是前者,这样的改法会让这段代码的全部逻辑失效;如果是后者,这样的改法会让部分逻辑失效。

更应该去做的,是去检查 += 的左边是否乱入了非 Button 的事件引发者。

grid.Click += OnButtonClick
button.Click += OnButtonClick;

修改这些源头上就已经不正确的代码才是真正解决问题

另一个角度,如果事件的引发者确实可能有多种,那么事件处理函数就应该加上 else 逻辑,或者不要再使用 sender,或者强制转换时使用基类型。这也是在真正的解决问题。

额外的,对于 OutOfMemoryException,这通常意味着“实现”部分的代码存在着性能问题,应该着手解决。

对于环境错误,关注于规避和恢复

环境错误是难以提前预估的;或者说预估的成本太高,不值得去预估。于是,当发生了环境错误,我们更加关注于这样的环境中是什么导致了异常,以及程序是否正确处理了这样的异常并恢复错误

.NET 中已经为我们准备了很多场景下的多套环境异常,例如 IO 相关的异常,网络连接相关的异常。这些异常都不是我们应该抛出的。

程序中的异常

在异常处理中,每一位开发者应该从根源上在自己的代码中消灭“实现异常”(而不是“逃避”),同时在“使用异常”的帮助下正确调用其他方法,那么代码中将只剩下“环境异常”(和小部分性能导致的“实现异常”)。

此时,开发者们将有更多的精力关注在“解决的具体业务”上面,而不是不停地解决编码上的 BUG。

特别的,“实现异常”可以被单元测试进行有效的检测。

本文会经常更新,请阅读原文: https://walterlv.com/post/throws-which-exception.html ,以避免陈旧错误知识的误导,同时有更好的阅读体验。

本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。欢迎转载、使用、重新发布,但务必保留文章署名 吕毅 (包含链接: https://walterlv.com ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。如有任何疑问,请 与我联系 (walter.lv@qq.com)

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏程序员互动联盟

【新技术分享】C++17 最新进展

C++标准委员会最近在夏威夷的科纳召开了一次会议,大家可能关心最新的进展,但是按照以往的情况,某些文件需要很久才会公开。会议进行的时候,大家都在忙着修订自己的文...

34960
来自专栏Coding01

Javascript 从异步函数到 Promise 到 Async/Await

我最近正在看的一本书《聊聊架构》,在进入今天的主题之前,我想和大家分享这本书里的一个概念“生命周期”。

18040
来自专栏C语言及其他语言

[每日一题]C语言程序设计教程(第三版)课后习题5.4

题目描述 有三个整数a b c,由键盘输入,输出其中的最大的数。 输入 一行数组,分别为a b c 输出 a b c其中最大的数 样例输入 10 20 30 样...

29640
来自专栏iKcamp

翻译连载 | 第 10 章:异步的函数式(下)-《JavaScript轻量级函数式编程》 |《你不知道的JS》姊妹篇

原文地址:Functional-Light-JS 原文作者:Kyle Simpson-《You-Dont-Know-JS》作者 第 10 章:异步的函数式(下)...

20250
来自专栏JavaEdge

设计模式实战 - 简单工厂

最可能给八卦炉下达什么样的生产命令呢? 应该是给我生产出一个黄色人种(YellowHuman类) 而不会是给我生产一个会走、会跑、会说话、皮肤是黄色的人种 ...

12550
来自专栏hrscy

202 - Swift 的核心是什么?

不知道大家有没有看过 WWDC 2015 的视频,其中有一个编号为 408 的视频解释了这个问题,下面是视频链接:Protocol-Oriented Progr...

14320
来自专栏后端技术探索

非Java程序员竟鲜有人真正理解DI和IOC

小编在后端圈也算是阅人无数了, 发现一个现象,Java程序员对于面向对象语言的基础知识整体掌握比较扎实,而类似PHP,Python的初级甚至中级程序员就比较薄弱...

22020
来自专栏Java技术栈

Java 10的10个新特性,将彻底改变你写代码的方式!

Java 9才发布几个月,很多玩意都没整明白,现在Java 10又要来了。。 这时候我真尼玛想说:线上用的JDK 7 甚至JDK 6,JDK 8 还没用熟,JD...

44280
来自专栏Jimoer

Java设计模式学习记录-模板方法模式

模板方法模式,定义一个操作中算法的骨架,而将一些步骤延迟到子类中。使得子类可以不改变一个算法的结构即可重新定义该算法的某些特定步骤。

19440
来自专栏小詹同学

为什么你的Python代码质量如此不堪……

作者:笑虎(Python爱好者,关注爬虫、数据分析、数据挖掘、数据可视化等) 原文链接:http://codebay.cn/post/7953.html

19640

扫码关注云+社区

领取腾讯云代金券