C#规范整理·异常与自定义异常

这里会列举在C#中处理CLR异常方面的规范,帮助大家构建和开发一个运行良好和可靠的应用系统。

640?wx_fmt=png

前言

  迄今为止,CLR异常机制让人关注最多的一点就是“效率”问题。其实,这里存在认识上的误区,因为正常控制流程下的代码运行并不会出现问题,只有引发异常时才会带来效率问题。基于这一点,很多开发者已经达成共识:不应将异常机制用于正常控制流中。达成的另一个共识是:CLR异常机制带来的“效率”问题不足以“抵消”它带来的巨大收益。
CLR异常机制至少有以下几个优点:

  • 正常控制流会被立即中止,无效值或状态不会在系统中继续传播。

  • 提供了统一处理错误的方法。

  • 提供了在构造函数、操作符重载及属性中报告异常的便利机制。

  • 提供了异常堆栈,便于开发者定位异常发生的位置。

  另外,“异常”其名称本身就说明了它的发生是一个小概率事件。所以,因异常带来的效率问题会被限制在一个很小的范围内。实际上,try catch所带来的效率问题几乎是可以忽略的。在某些特定的场合,如Int32的Parse方法中,确实存在着因为滥用而导致的效率问题。在这种情况下,我们就应该考虑提供一个TryParse方法,从设计的角度让用户选择让程序运行得更快。另一种规避因为异常而影响效率的方法是:Tester-doer模式

正文

1.用抛出异常代替返回错误代码

在异常机制出现之前,应用程序普遍采用返回错误代码的方式来通知调用者发生了异常。本建议首先阐述为什么要用抛出异常的方式来代替返回错误代码的方式。对于一个成员方法而言,它要么执行成功,要么执行失败。成员方法执行成功的情况很容易理解,但是如果执行失败了却没有那么简单,因为我们需要将导致执行失败的原因通知调用者。抛出异常和返回错误代码都是用来通知调用者的手段。

但是当我们想要告诉调用者更多细节的时候,就需要与调用者约定更多的错误代码。于是我们很快就会发现,错误代码飞速膨胀,直到看起来似乎无法维护,因为我们总在查找并确认错误代码。
在没有异常处理机制之前,我们只能返回错误代码。但是,现在有了另一种选择,即使用异常机制。如果使用异常机制,那么最终的代码看起来应该是下面这样的:

640?wx_fmt=png

使用CLR异常机制后,我们会发现代码变得更清晰、更易于理解了。至于效率问题,还可以重新审视“效率”的立足点:throw exception产生的那点效率损耗与等待网络连接异常相比,简直微不足道,而CLR异常机制带来的好处却是显而易见的。

这里需要稍加强调的是,在catch(CommunicationExcep-tion)这个代码块中,代码所完成的功能是“通知发送”而不是“发送”本身,因为我们要确保在catch和finally中所执行的代码是可以被执行的。换句话说,尽量不要在catch和finally中再让代码“出错”,那会让异常堆栈信息变得复杂和难以理解。

在本例的catch代码块中,不要真的编写发送邮件的代码,因为发送邮件这个行为可能会产生更多的异常,而“通知发送”这个行为稳定性更高(即不“出错”)。

以上通过实际的案例阐述了抛出异常相比于返回错误代码的优越性,以及在某些情况下错误代码将无用武之地,如构造函数、操作符重载及属性。语法特性决定了其不能具备任何返回值,于是异常机制被当做取代错误代码的首要选择。

2.不要在不恰当的场合下引发异常

程序员,尤其是类库开发人员,要掌握的两条首要原则是:
正常的业务流程不应使用异常来处理。
不要总是尝试去捕获异常或引发异常,而应该允许异常向调用堆栈往上传播。
那么,到底应该在怎样的情况下引发异常呢?

第一类情况 如果运行代码后会造成内存泄漏、资源不可用,或者应用程序状态不可恢复,则应该引发异常。
在微软提供的Console类中有很多类似这样的代码:

640?wx_fmt=png

在开头首先提到的就是:对在可控范围内的输入和输出不引发异常。没错,区别就在于“可控”这两个字。所谓“可控”,可定义为:发生异常后,系统资源仍可用,或资源状态可恢复。

第二类情况 在捕获异常的时候,如果需要包装一些更有用的信息,则引发异常。
这类异常的引发在UI层特别有用。系统引发的异常所带的信息往往更倾向于技术性的描述;而在UI层,面对异常的很可能是最终用户。如果需要将异常的信息呈现给最终用户,更好的做法是先包装异常,然后引发一个包含友好信息的新异常。

第三类情况 如果底层异常在高层操作的上下文中没有意义,则可以考虑捕获这些底层异常,并引发新的有意义的异常。
例如在下面的代码中,如果抛出InvalidCastException,则没有任何意义,甚至会造成误解,所以更好的方式是抛出一个ArgumentException:

需要重点介绍的正确引发异常的典型例子就是捕获底层API错误代码,并抛出。查看Console这个类,还会发现很多地方有类似的代码:

640?wx_fmt=png

Console为我们封装了调用Windows API返回的错误代码,而让代码引发了一个新的异常。

很显然,当需要调用Windows API或第三方API提供的接口时,如果对方的异常报告机制使用的是错误代码,最好重新引发该接口提供的错误,因为你需要让自己的团队更好地理解这些错误。

3.重新引发异常时使用Inner Exception

当捕获了某个异常,将其包装或重新引发异常的时候,如果其中包含了Inner Exception,则有助于程序员分析内部信息,方便代码调试。
以一个分布式系统为例,在进行远程通信的时候,可能会发生的情况有:
1)网卡被禁用或网线断开,此时会抛出SocketException,消息为:“由于目标计算机积极拒绝,无法连接。”
2)网络正常,但是要连接的目标机没有端口没有处在侦听状态,此时,会抛出SocketException,消息为:“由于连接方在一段时间后没有正确答复或连接的主机没有反应,连接尝试失败。”
3)连接超时,此时需要通过代码实现关闭连接,并抛出一个SocketException,消息为:“连接超过约定的时长。”
发生以上三种情况中的任何一种情况,在返回给最终用户的时候,我们都需要将异常信息包装成为“网络连接失败,请稍候再试”。

所以,一个分布式系统的业务处理方法,看起来应该是这样的:

640?wx_fmt=png

但是,在提示这条消息的时候,我们可能需要将原始异常信息记录到日志里,以供开发者分析具体的原因(因为如果这种情况频繁出现,这有可能是一个Bug)。那么,在记录日志的时候,就非常有必要记录导致此异常出现的内部异常或是堆栈信息。
上文代码中的:就是将异常重新包装成为一个CommucationFailureException,并将SocketException作为Inner Exception(即err)向上传递。

此外还有一个可以采用的技巧,如果不打算使用Inner Exception,但是仍然想要返回一些额外信息的话,可以使用Exception的Data属性。如下所示:

640?wx_fmt=png

4.避免在finally内撰写无效代码

你应该始终认为finally内的代码会在方法return之前执行,哪怕return是在try块中。
C#编译器会清理那些它认为完全没有意义的C#代码。

640?wx_fmt=png

5.避免嵌套异常

应该允许异常在调用堆栈中往上传播,不要过多使用catch,然后再throw。过多使用catch会带来两个问题:

  • 代码更多了。这看上去好像你根本不知道该怎么处理异常,所以你总在不停地catch。

  • 隐藏了堆栈信息,使你不知道真正发生异常的地方。

嵌套异常会导致 调用堆栈被重置了。最糟糕的情况是:如果方法捕获的是Exception。所以也就是说,如果这个方法中还存在另外的异常,在UI层将永远不知道真正发生错误的地方。
除了第3点提到的需要包装异常的情况外,无故地嵌套异常是我们要极力避免的。当然,如果真的需要捕获这个异常来恢复一些状态,然后重新抛出,代码看起来应该是这样的:

640?wx_fmt=png

尽量避免像下面这样引发异常:

直接throw err而不是throw将会重置堆栈信息。

6.避免“吃掉”异常

嵌套异常是很危险的行为,一不小心就会将异常堆栈信息,也就是真正的Bug出处隐藏起来。但这还不是最严重的行为,最严重的就是“吃掉”异常,即捕获,然后不向上层throw抛出。如果你不知道如何处理某个异常,那么千万不要“吃掉”异常,如果你一不小心“吃掉”了一个本该往上传递的异常,那么,这里可能诞生一个Bug,而且,解决它会很费周折。

避免“吃掉”异常,并不是说不应该“吃掉”异常,而是这里面有个重要原则:该异常可被预见,并且通常情况它不能算是一个Bug。比如有些场景存在你可以预见的但不重要的Exception,这个就不算一个bug。

7.为循环增加Tester-Doer模式而不是将try-catch置于循环内

如果需要在循环中引发异常,你需要特别注意,因为抛出异常是一个相当影响性能的过程。应该尽量在循环当中对异常发生的一些条件进行判断,然后根据条件进行处理。

8.总是处理未捕获的异常

处理未捕获的异常是每个应用程序应具备的基本功能,C#在AppDomain提供了UnhandledException事件来接收未捕获到的异常的通知。常见的应用如下:

640?wx_fmt=png

未捕获的异常通常就是运行时期的Bug,我们可以在App-Domain.CurrentDomain.UnhandledException的注册事件方法CurrentDomain_UnhandledException中,将未捕获异常的信息记录在日志中。值得注意的是,UnhandledException提供的机制并不能阻止应用程序终止,也就是说,执行CurrentDomain_UnhandledException方法后,应用程序就会被终止。

9.正确捕获多线程中的异常

多线程的异常处理需要采用特殊的方法。以下的处理方式会存在问题:

640?wx_fmt=png

应用程序并不会在这里捕获线程t中的异常,而是会直接退出。从.NET 2.0开始,任何线程上未处理的异常,都会导致应用程序的退出(先会触发AppDomain的UnhandledException)。上面代码中的try-catch实际上捕获的还是当前线程的异常,而t属于新起的异常,所以,正确的做法应该是把 try-catch放在线程里面

640?wx_fmt=png

10.慎用自定义异常

除非有充分的理由,否则一般不要创建自定义异常。如果要对某类程序出错信息做特殊处理,那就自定义异常。需要自定义异常的理由如下:
1)方便调试。通过抛出一个自定义的异常类型实例,我们可以使捕获代码精确地知道所发生的事情,并以合适的方式进行恢复。
2)逻辑包装。自定义异常可包装多个其他异常,然后抛出一个业务异常。
3)方便调用者编码。在编写自己的类库或者业务层代码的时候,自定义异常可以让调用方更方便处理业务异常逻辑。例如,保存数据失败可以分成两个异常“数据库连接失败”和“网络异常”。
4)引入新异常类。这使程序员能够根据异常类在代码中采取不同的操作。

11.从System.Exception或其他常见的基本异常中派生异常

这个不说了,自定义异常一般是从System.Exception派生。。事实上,现在如果你在Visual Studio中输入Exception,然后使用快捷键Tab,VS会自动创建一个自定义异常类。

12.应使用finally避免资源泄漏

前面已经提到过,除非发生让应用程序中断的异常,否则finally总是会先于return执行。finally的这个语言特性决定了资源释放的最佳位置就是在finally块中;另外,资源释放会随着调用堆栈由下往上执行(即由内到外释放)。

13.避免在调用栈较低的位置记录异常

即避免在内部深处处理记录异常。最适合记录异常和报告的是应用程序的最上层,这通常是UI层。
并不是所有的异常都要被记录到日志,一类情况是异常发生的场景需要被记录,还有一类就是未被捕获的异常。未被捕获的异常通常被视为一个Bug,所以,对于它的记录,应该被视为系统的一个重要组成部分。

如果异常在调用栈较低的位置被记录或报告,并且又被包装后抛出;然后在调用栈较高位置也捕获记录异常。这就会让记录重复出现。在调用栈较低的情况下,往往异常被捕获了也不能被完整的处理。所以,综合考虑,应用程序在设计初期,就应该为开发成员约定在何处记录和报告异常。

640?wx_fmt=jpeg

原文地址:https://www.cnblogs.com/zhan520g/p/11072519.html

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/315302.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Codeforces Round #619 (Div. 2) D. Time to Run 矩阵回路构造

传送门 文章目录题意:思路:题意: 给你一个n∗mn*mn∗m的矩阵,每两个相邻点之间有个双向边,问你能不能走满kkk步,每一步走的边不同,但是点可以相同,换句话说就是走的边不能再走了。输…

HDU - 6756 Finding a MEX-分块思想

https://vjudge.net/problem/HDU-6756 题目大意:给你一个无向图,每个点有权值a,将f(u)定义为对u的邻居的集合求mex; 有两个操作: 1:将u的权值修改为x 2:查询f&#x…

中高级数论 [欧拉函数线性筛,二次剩余]

欧拉函数线性筛 对于素数ppp, φ(p∗i){p−1i1p∗φ(i)p∣i(p−1)∗φ(i)p∤i\varphi (p*i) \begin{cases} p-1& i1\\ p*\varphi(i)& p \mid i\\ (p-1)*\varphi(i) & p \nmid i \end{cases}φ(p∗i)⎩⎪⎨⎪⎧​p−1p∗φ(i)(p−1)∗φ(i)​i1p∣ip∤i​ 证明&am…

C#中await/async闲说

自从C#5.0增加异步编程之后,异步编程越来越简单,async和await用的地方越来越多,越来越好用,只要用异步的地方都是一连串的异步,如果想要异步编程的时候,需要从底层开始编写,这样后边使用的时候就…

Codeforces Round #619 (Div. 2) E. Nanosoft 思维 + 二维前缀和

传送门 文章目录题意:思路:题意: 思路: 考虑到最大面积是由四种颜色构成的,且四种颜色可以从中心扩展出去,所以我们分别维护四种颜色的二维前缀和,O(1)O(1)O(1)计算矩阵内颜色的个数。现在我们…

CF 613D - Kindom and its Cities-虚树

https://codeforces.com/problemset/problem/613/D 题目大意:有n个点,每次询问要最少移除多少个点使得关键点分隔 思路:首先,如果关键点是相连的话,那么一定是不行的。 然后因为询问的总节点数不多,可以…

FFT:从入门到沉迷

终于学会了FFTFFTFFT并无法自拔 啥是FFT FFTFFTFFT指快速傅里叶变换 在OIOIOI中的应用是O(nlogn)O(nlogn)O(nlogn)计算函数卷积 人话:多项式乘法 多项式毒瘤模板题的万恶之源 系数表达式与点值表达式 系数表达式就是平常的表示方法 f(x)∑i0naixif(x)\sum_{i0…

HDU - 6769-In Search of Gold-二分+树形dp

https://vjudge.net/problem/HDU-6769 题目大意:给你n个点,有n-1条边,每条边有a,b两个权值,给你一个k,恰好有k条边的权值取a,其余取b的时候,树的直径的最小值。 思路:答…

Codeforces Round #701 (Div. 2) E. Move and Swap 思维 + dp

传送门 文章目录题意:思路:题意: 思路: 由于是按层来的,所以我们肯定先按照层来分组。 定义dp[i]dp[i]dp[i]为红棋在位置iii的时候的最大得分和。 先考虑不换的情况,我们对于每个点都从他的父节点转移过来…

用 docker-compose 启动 WebApi 和 SQL Server

本系列文章所要做出的演示架构基于 .NET Core Web Api、MSSQL、Skywalking 和 nginx ,这些都会通过docker-compose一键创建/启动容器,然后用 Azure DevOps 发布上线。所以本系列文章重点并不是如何写好.NET Core,而是围绕着 .NET Core 的容器…

OI群论:从入门到自闭

怎么这么多阅读啊…… 这篇文章是群论(其实只有Polya)在信息学奥赛中计数的运用,并不是群论的讲义…… 群论可以说是数学中最高深的内容,一个oier去深入了解是不现实的。因此,我们只需要知道结论。 本文主要讲解群论…

2020牛客暑期多校训练营(第二场)Just Shuffle

https://ac.nowcoder.com/acm/contest/5667/J 题目大意:给你一个置换A,使得置换P^kA,让你求出置换P。 思路:我们根据置换A再置换z次,那么就等于置换p 置换z*k次,如果z*k%len0,那么将会回到单位序列,那我们…

Codeforces Round #619 (Div. 2) F. Super Jaber 多源bfs + 思维转换

传送门 文章目录题意:思路:题意: 给你一个矩阵,每个格子都有一个颜色kkk,每秒可以移动到相邻矩阵或者瞬移到同一颜色的任意矩阵。有qqq个询问,每次询问给出两个点,求从一个点到另一个点的最短时…

你可以保持沉默,但你所说的一切都将成为呈堂证供——浅谈Azure WORM保护

本文作者|Yuan Han本文来源|Reid爸的菜园子美国安然事件后,电子数据的合规性保存越来越受到重视;各国政府制定了一系列的法律,如美国《赛班斯法案》等,对于不同类型的电子数据保留期限做了严格规定;国内也没落后&#…

后缀自动机:从入门到放弃

写在前面 后缀自动机,简称SAMSAMSAM,是一种十分优秀的字符串匹(shu)配(ju)算(jie)法(gou) 字符串界的bossbossboss,几乎可以解决全部正常的字符串题目 至少我前前后后学了一年,听过444次课,几度怀疑自己不适合oioioi 请做好心…

2021牛客第一场 K.Knowledge Test about Match

https://ac.nowcoder.com/acm/contest/11166/K 题意就是使得图中的那个式子最小,你的答案不一定是要最标准的,只要平均水平下和标准值的偏差不超过4%就行了。 有了这个提示,那我们直接贪心瞎搞就行了,只有符合换过去的收益的增大…

Codeforces Round #620 (Div. 2) F2. Animal Observation (hard version) dp + 线段树

传送门 文章目录题意:思路:题意: 比如下面这个图: 思路: 对于这个题,比较容易就能考虑到dpdpdp,设f[i][j]f[i][j]f[i][j]为到了第iii行,覆盖了[j,jk−1][j,jk-1][j,jk−1]范围时候…

设计模式之总体介绍

1. 背景与介绍设计模式是经过反复使用、经过分类的代码总结。设计模式的目的是提高代码可重用性和可靠性,并使代码条理清晰、易于理解、易于维护。设计模式描述了在各种情况下,要选择什么样的方案来解决问题。设计模式通常以类和对象来描述其中的关系和相…

回文自动机:从入门到只会打板

写在前面 如果你会SAMSAMSAM,相信回文自动机不会难懂。 如果你不会,你可以参考我的上一篇文章。 至少回文自动机是治愈系的吧。 作用 回文自动机,也叫回文树,简称PAMPAMPAM实际上它既不是自动机也不是树 处理回文串的有力工…

2021牛客第一场 I. Increasing Subsequence-前缀和优化dp

https://ac.nowcoder.com/acm/contest/11166/I 思路:dp[i][j] 是表示上上步走在i点,上一步走在j点的期望。首先我们很容易想到n^3的做法,那我们必须考虑去优化一维的时间复杂度。我们可以考虑使用前缀和优化dp转移。 我们枚举i点&#xff0c…