应该抛出什么异常?不应该抛出什么异常?(.NET/C#)
我在 .NET/C# 建议的异常处理原则 中描述了如何 catch
异常以及重新 throw
。然而何时应该 throw
异常,以及应该 throw
什么异常呢?
究竟是谁错了?
代码中从上到下从里到外都是在执行一个个的包含某种目的的代码,我们将其称之为“任务”。当需要完成某项任务时,任务的完成情况只有两种结果:
- 成功完成
- 失败
异常处理机制就是处理上面的第 2 种情况。这里我们不谈论错误码系统,那么,异常便应该在任务执行失败时抛出异常。
抛出异常后,报告错误只是手段,真正要做的是帮助开发者修复错误。于是,第一个要做的就是区分到底——谁错了!
- 任务的使用者用错了
- 任务的执行代码写错了
- 任务执行时所在的环境不符合预期
简单说来,就是:使用错误,实现错误、环境错误。
让我们把异常归类到这些错误中
本文的重点在于指导我们何时应该抛出什么异常,也就是说——我们的角色是——任务的编写者。那么,编写者有责任编写出一段没有错误的代码。这就说明——永远不应该抛出表示自己写错了的异常。
那么,我们对常见的异常进行分类。
使用错误
ArgumentException
表示参数使用错了ArgumentNullException
表示参数不应该传入null
ArgumentOutOfRangeException
表示参数中的序号超出了范围InvalidEnumArgumentException
表示参数中的枚举值不正确
InvalidOperationException
表示当前状态下不允许进行此操作(也就是说存在着允许进行此操作的另一种状态)ObjectDisposedException
表示对象已经Dispose
过了,不能再使用了
NotSupportedException
表示不支持进行此操作(这是在说不要再试图对这种类型的对象调用此方法了,不支持)PlatformNotSupportedException
表示在此平台下不支持(如果程序跨平台的话)
实现错误
NullReferenceException
试图在空引用上执行某些方法,除了告诉实现者出现了意料之外的null
之外,没有什么其它价值了IndexOutOfRangeException
使用索引的时候超出了边界InvalidCastException
表示试图对某个类型进行强转但类型不匹配StackOverflow
表示栈溢出,这通常说明实现代码的时候写了不正确的显式或隐式的递归OutOfMemoryException
表示托管堆中已无法分出期望的内存空间,或程序已经没有更多内存可用了AccessViolationException
这说明使用非托管内存时发生了错误BadImageFormatException
这说明了加载的 dll 并不是期望中的托管 dllTypeLoadException
表示类型初始化的时候发生了错误
环境错误
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
。但是,当拿着这样的异常去调查哪里错了的时候,我们会发现 demo
和 anotherDemo
都可能为 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();
}
当然,不像 ArgumentNullException
,InvalidOperationException
通常并不一定能在开始就确定是否满足状态要求,但最好能尽可能在第一时间抛出,避免错误蔓延。
做到了第一时间抛出使用错误,就能让使用者明确知道自己用错了,需要修改使用代码。(这正是被另外一项事实所逼——典型的程序员是不看文档的,“使用异常”代替了一部分文档。)
永远不应该让实现错误抛出
这一节的标题其实说了三件事情:
- 永远不应该主动用
throw
句式抛出“实现错误”章节中提到的任何异常 - 如果你在调用某个别人实现的代码时遇到了“实现错误”章节中提到的异常,那说明“那个人”的代码写出 BUG 了,确信无疑。
- 如果自己写的代码发现抛出了这些异常,那就说明自己写出了 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。
特别的,“实现异常”可以被单元测试进行有效的检测。
应该抛出什么异常?不应该抛出什么异常?(.NET/C#)的更多相关文章
- Atitti 跨语言异常的转换抛出 java js
Atitti 跨语言异常的转换抛出 java js 异常的转换,直接反序列化为json对象e对象即可.. Js.没有完整的e机制,可以参考java的实现一个stack层次机制的e对象即可.. 抛出Ru ...
- ORACLE 存储过程异常捕获并抛出
for tab_name in tables loop execute immediate 'drop table '||tab_name; --此处可能会报错 end loop; 当前情况是,循环表 ...
- 异常0xc000041d的抛出过程
为了说明这个过程,我们必须写一个示例程序,如下: #include "stdafx.h" #include <tchar.h> #include <stdio.h ...
- java-异常-自定义异常异常类的抛出throws
1 package p1.exception; 2 /* 3 * 对于角标是整数不存在,可以用角标越界表示, 4 * 对于负数为角标的情况,准备用负数角标异常来表示. 5 * 6 * 负数角标这种异常 ...
- 获取异常信息里再出异常就找不到日志了,我TM人傻了
本系列是 我TM人傻了 系列第三期[捂脸],往期精彩回顾: 升级到Spring 5.3.x之后,GC次数急剧增加,我TM人傻了 这个大表走索引字段查询的 SQL 怎么就成全扫描了,我TM人傻了 最近组 ...
- JAVA之旅(十)——异常的概述,Try-Catch,异常声明Throws,多异常处理,自定义异常,Throw和Throws的区别
JAVA之旅(十)--异常的概述,Try-Catch,异常声明Throws,多异常处理,自定义异常,Throw和Throws的区别 不知不觉,JAVA之旅这个系列已经更新到第十篇了,感觉如梦如幻,时间 ...
- java的异常和java web容器的异常
一.java的异常,只要catch住异常了,程序就不会挂,依然会执行catch之后的语句 Java程序发生异常就挂了吗? 为了验证程序不会挂,我写了个例子给大家看看. 测试代码: import jav ...
- python——异常except语句用法与引发异常
except: #捕获所有异常 except: <异常名>: #捕获指定异常 except:<异常名1,异常名2):捕获异常1或者异常2 except:<异常名>,< ...
- 异常-try...catch的方式处理异常2
package cn.itcast_02; /* * A:一个异常 * B:二个异常的处理 * a:每一个写一个try...catch * b:写一个try,多个catch * try{ * ... ...
- C++异常之七 标准库里的异常类
标准库里的异常类 C++标准提供了一组标准异常类,这些类以基类 Exception 开始,标准程序库抛出的所有异常,都派生于该基类,这些类构成如图所示的异常类的派生继承关系,该基类提供一个成员函数 w ...
随机推荐
- spring mvc:练习 @RequestParam(参数绑定到控制器)和@PathVariable(参数绑定到url模板变量)
spring mvc:练习 @RequestParam和@PathVariable @RequestParam: 注解将请求参数绑定到你的控制器方法参数 @PathVariable: 注释将一个方法参 ...
- Selenium元素定位问题
定位元素时,遇到一些诡异事件: 明明就是通过ID定位的,但是就是没有定位到该元素呢? 1.通过element.find_elements_by_xxx()获取该元素的个数,试试是否有获取到元素,0个表 ...
- 新男人八题---AStringGame
终于完成进度男人1/8,为了这题学了sam= = 题意先有一个串,n个子串,两个人轮流每次在子串上加字符,要求加完后还是原串的子串,最后不能加的就是输者,求赢的人 解法:sam之后在构造的状态图上跑s ...
- 总结网站Mysql优化
Mysql存储引擎 选择合适的存储引擎Innodb myisam myisam: 写入数据非常快,适合使用场合dedecms/phpcms/discuz/微博系统等写入.读取操作多的系统. inno ...
- 获取CPU和内存的使用率
1.获取CPU的使用率 主要就是一个计算. int CUseRate::GetCPUUseRate() //获取CPU使用率 { ; FILETIME ftIdle, ftKernel, ftUser ...
- opencv 图片降噪
—— # -*- coding: utf-8 -* import numpy as np import cv2 cap = cv2.VideoCapture(0) while True: _ , fr ...
- 用Qt写了一个qq客户端,采用webqq协议,发出来和大家分享一下---大神请无视
首先做以下声明: 本程序基于腾讯公司的webqq协议开发,所有相关版权归腾讯公司所有.此程序只用于技术交流和学习,不得用于其他方面. ---开发者:雨后星辰,转载请注明出处:http://www.cn ...
- S16课件
Python之路,Day1 - Python基础1 介绍.基本语法.流程控制 Python之路,Day2 - Python基础2 列表.字典.集合 Python之路,Day3 - Python基础3 ...
- mysqldb 安装
MySQLdb是python的一个标准的连接和操纵mysql的模块. ubuntu下安装: sudo apt-get install python-mysqldb sudo apt-get insta ...
- 转载:【Oracle 集群】RAC知识图文详细教程(二)--Oracle 集群概念及原理
文章导航 集群概念介绍(一) ORACLE集群概念和原理(二) RAC 工作原理和相关组件(三) 缓存融合技术(四) RAC 特殊问题和实战经验(五) ORACLE 11 G版本2 RAC在LINUX ...