.NET异常设计原则

异常是使用.NET时必然会遇到的问题,但是,有太多的开发人员没有从API设计的角度考虑这个问题。在大部分工作中,他们自始至终都知道需要捕获什么异常以及哪些异常需要写入全局日志。如果你设计了可以让你正确使用异常的API,则可以显著减少修复缺陷的时间。

谁的错?

异常设计背后的基本理论始于这样一个问题,“谁的错?”为了方便本文的讨论,这个问题的答案将总是以下三者之一:

  • 应用程序

  • 环境

当我们说“库”有问题,我们是指当前执行的某个方法有内部缺陷。在这种情况下,“应用程序”是调用库方法的代码(这有点混杂难分,因为库和应用程序代码可能在相同的程序集中。)最后,“环境”是指应用程序之外一切无法控制的东西。

库缺陷

最典型的库缺陷是NullReferenceException。对库而言,它没有任何理由抛出可以被应用程序检测到的空引用异常。如果遇到了空,则库代码应该总是抛出一个更具体的异常,说明什么为空以及如何纠正这个问题。对于参数而言,这显然是一个ArgumentNullException异常。而如果属性或字段为空,则InvalidOperationException通常更合适。

根据定义,任何表明库缺陷的异常都是该库中需要修复的Bug。那并不是说应用程序代码没有Bug,而是说库的Bug需要首先修复。只有那样,才能让应用程序开发人员知道他也犯了错误。

这样做的原因是,可能有许多人使用同样的库。如果一个人在不应该传入空的地方错误地传入了空,则其他人想必也会犯同样的错误。把NullReferenceException替换为一个可以清晰地显示出什么出错的异常,应用程序开发人员立即就可以知道什么出错了。

“成功之核(The Pit of Success)”

如果你读过有关.NET设计模式的早期文献,那么你会经常碰到短语“成功之核”。其基本思想是这样的:让代码容易被正确使用,不容易被误用,并确保异常可以告诉你哪里出错了。遵循这个API设计理念,几乎可以保证开发人员一开始就编写出正确的代码。

这就是为什么一个没有注释的NullReferenceException是如此糟糕。除了堆栈跟踪外(可能非常深入库代码),没有任何信息可以帮助开发人员确定他们哪里做错了。另一方面,ArgumentNullException和InvalidOperationException则为库作者提供了一种方法,让他们可以向应用程序开发人员说明如何修复问题。

其他库缺陷

下一个库缺陷是ArithmeticException系列,包括DivideByZeroException、FiniteNumberException和OverflowException。再次,这总是意味着库方法的内部缺陷,即使那个缺陷只是一个缺失的参数有效性检查。

库缺陷的另外一个例子是IndexOutOfRangeException。从语义上讲,它和ArgumentOutOfRangeException没什么不同,参见IList.Item,但它只适用于数组索引器。由于应用程序代码通常不会使用裸数组,所以这意味着,自定义的集合类会有Bug。

自.NET 2.0引入泛型列表以来,ArrayTypeMismatchException就很少见了。触发该异常的情况相当怪异。根据文档:

当系统无法将数组元素转换成声明的数组类型时会抛出ArrayTypeMismatchException。例如,一个String类型的元素无法存入一个Int32数组,因为这两种类型之间无法转换。应用程序一般是不需要抛出这类异常的。

要做到这一点,前面提到的Int32数组必须存入一个Object[]类型的变量。如果你使用了原始数组,则库需要对此进行检查。由于这个原因及其他许多方面的考虑,最好是不要使用原始数组,而是将它们封装到一个合适的集合类中。

通常,其他转换问题是通过InvalidCastException异常反映出来的。回到我们的主题,类型检查应该意味着永远不会抛出InvalidCastException异常,而是向调用者抛出ArgumentException或InvalidOperationException异常。

MemberAccessException是一个基类,涵盖了各种基于反射的错误。除了直接使用反射外,COM互操作和动态关键词的不正确使用都会触发该异常。

应用程序缺陷

典型的应用程序缺陷是ArgumentException及其子类ArgumentNullException和ArgumentOutOfRangeException。以下是其他你可能不知道的子类:

  • System.ComponentModel.InvalidAsynchronousStateException

  • System.ComponentModel.InvalidEnumArgumentException

  • System.DuplicateWaitObjectException

  • System.Globalization.CultureNotFoundException

  • System.IO.Log.ReservationNotFoundException

  • System.Text.DecoderFallbackException

  • System.Text.EncoderFallbackException

所有这些都明确地表明应用程序有错误,而问题就出在调用库方法的行里。那条语句的两个部分都很重要。考虑下面的代码:

foo.Customer = null; foo.Save();

如果上述代码抛出了一个ArgumentNullException异常,那么应用程序开发人员会很困惑。它应该抛出一个InvalidOperationException异常,说明当前行之前有什么地方出了问题。

以异常为文档

典型的程序员不阅读文档,至少不会首先阅读文档。相反,他或她会阅读公共API,编写一些代码并运行。如果代码不能正常运行,就到Stack Overflow上搜索异常信息。如果该程序员够幸运,则很容易在那里找到答案以及指向正确文档的链接。但即使如此,程序员们很可能也不会真正地读它。

那么,作为库作者,我们如何解决这个问题?第一步是直接将部分文档复制到异常中。

更多对象状态异常

InvalidOperationException有一个众所周知的子类ObjectDisposedException。它的用途显而易见,然而,很少有可销毁类会忘记抛出这个异常。如果忘记了,则常见的结果是抛出NullReferenceException异常。该异常是由Dispose方法将可销毁子对象置为空所导致的。

与InvalidOperationException密切相关的是NotSupportedException异常。这两种异常很容易区分:InvalidOperationException是指“你现在不能那样操作”,而NotSupportedException是指“你永远不能对这个类做那种操作”。理论上讲,NotSupportedException应该只在使用抽象接口时出现。

例如,一个不可变集合在遇到IList.Add方法时应该抛出NotSupportedException异常。相比之下,一个可冻结集合在冻结状态下遇到该方法时会抛出InvalidOperationException异常。

NotSupportedException一个越来越重要的子类是PlatformNotSupportedException。该异常表示,操作可以在某些运行环境里进行,但不能在其他环境里进行。例如,当将代码从.NET移植到UWP或.NET Core时,你可能需要使用这个异常,因为它们没有提供.NET Framework的所有特性。

难以捉摸的FormatException

微软在设计.NET的第一个版本时犯了一些错误。例如,从逻辑上讲,FormatException是一个参数异常类型,甚至文档也说“该异常是在参数格式无效时抛出”。但是,不管出于什么原因,它实际上没有继承ArgumentException。它也没有地方存放参数名称。

我们暂时提供的建议是不要抛出FormatException异常,而是自己创建ArgumentException的子类,可以命名为“ArgumentFormatException”或其他效果类似的名称。这可以为你提供必要的信息,如参数名称和实际使用的值,减少调试时间。

这把我们带回了最初的主题“异常设计”。是的,当你自行开发的解析器检测到了问题,你可以只抛出一个FormatException异常,但那无法为想要使用你的库的应用程序开发人员提供帮助。

有关这个框架设计缺陷,另外一个例子是IndexOutOfRangeException。从语义上讲,它和ArgumentOutOfRangeException没什么不同,然而,这个特例只是针对数组索引器吗?不,那样想就错了。看下IList.Item的实例集,该方法只会抛出ArgumentOutOfRangeException异常。

环境缺陷

环境缺陷源于世界并不完美这样一个事实,诸如数据宕机、Web服务器无响应、文件丢失等场景。当Bug报告中出现环境缺陷时,需要考虑以下两个方面:

  1. 应用程序正确地处理了缺陷吗?

  2. 在这个环境里,是什么导致了缺陷?

通常,这会涉及人员分工。首先,应用程序开发人员应该第一个查找问题的答案。这不仅仅是说要处理错误并恢复,而且要生成一个有用的日志。

你可能想知道,为什么要从应用程序开发人员开始。应用程序开发人员要对运维团队负责。如果一次Web服务器调用失败,则应用程序开发人员不能只是甩手大叫“不是我的问题”。他或她首先需要确保异常提供了足够的细节信息,让运维人员可以开展他们的工作。如果异常仅仅提供了“服务器连接超时”的信息,那么他们怎么能知道涉及了哪台服务器?

专用异常

NotImplementedException

NotImplementedException表示且仅表示一件事:这项特性还在开发过程中。因此,NotImplementedException提供的信息应该总是包含一个任务跟踪软件的引用。例如:

throw new NotImplementedException("参见工单#42.");

你可以提供更详细的信息,但实际上,你记录的任何信息几乎立刻就会过期。因此,最好是只将读者导向工单,他们可以在那里看到诸如该特性按计划将会在何时实现这样的信息。

AggregateException

AggregateException是必要之恶,但很难使用。它本身不包含任何有价值的信息,所有的细节信息都隐藏在它的InnerExceptions集合中。

由于AggregateException通常只包含一个项,所以在库中将它解封装并返回真正的异常似乎是合乎逻辑的。一般来说,你不能在没有销毁原始堆栈跟踪的情况下再次抛出一个内部异常,但从.NET 4.5开始,该框架提供了使用ExceptionDispatchInfo的方法。

解封装AggregateException
catch (AggregateException ex) {   
  if (ex.InnerExceptions.Count == 1) //解封装       
      ExceptionDispatchInfo.Capture(ex.InnerExceptions[0]).Throw();  
  else     
     throw; //我们真的需要AggregateException }
无法回答的情况

有一些异常无法简单地纳入这个主题。例如,AccessViolationException表示读取非托管内存时有问题。对,那可能是由原生库代码所导致的,也可能是由应用程序错误地使用了同样的代码库所导致的。只有通过研究才能揭示这个Bug的本质。

如果可能,你就应该在设计时避免无法回答的异常。在某些情况下,Visual Studio的静态代码分析器甚至可以分析该规则所涵盖的标识冲突。

例如,ApplicationException实际上已经废弃。Framework设计指南明确指出,“不要抛出或继承ApplicationException。”为此,应用程序不必抛出ApplicationException异常。虽说初衷如此,但看下下面这些子类:

  • Microsoft.JScript.BreakOutOfFinally

  • Microsoft.JScript.ContinueOutOfFinally

  • Microsoft.JScript.JScriptException

  • Microsoft.JScript.NoContextException

  • Microsoft.JScript.ReturnOutOfFinally

  • System.Reflection.InvalidFilterCriteriaException

  • System.Reflection.TargetException

  • System.Reflection.TargetInvocationException

  • System.Reflection.TargetParameterCountException

  • System.Threading.WaitHandleCannotBeOpenedException

显然,这些子类中有一些应该是参数异常,而其他的则表示环境问题。它们全都不是“应用程序异常”,因为他们只会被.NET Framework的库抛出。

同样的道理,开发人员不应该直接使用SystemException。同ApplicationException一样,SystemException的子类也是各不相同,包括ArgumentException、NullReferenceException和AccessViolationException。微软甚至建议忘掉SystemException的存在,而只使用其子类。

无法回答的情况有一个子类别,就是基础设施异常。我们已经看过AccessViolationException,以下是其他的基础设施异常:

  • CannotUnloadAppDomainException

  • BadImageFormatException

  • DataMisalignedException

  • TypeLoadException

  • TypeUnloadedException

这些异常通常很难诊断,可能会揭示出库或调用它的代码中存在的难以理解的Bug。因此,和ApplicationException不同,把它们归为无法回答的情况是合理的。

实践:重新设计SqlException

请记住这些原则,让我们看下SqlException。除了网络错误(你根本无法到达服务器)外,在SQL Server的master.dbo.sysmessages表中有超过11000个不同的错误代码。因此,虽然该异常包含了你需要的所有底层信息,但是,除了简单地捕获&记录外,你实际上难以做任何事。

如果我们要重新设计SqlException,那么我们会希望,根据我们期望用户或开发人员做什么,将其分解成多个不同的类别。

SqlClient.NetworkException会表示所有说明数据库服务器本身之外的环境存在问题的错误代码。

SqlClient.InternalException会包含说明服务器存在严重故障(如数据库损坏或无法访问硬盘)的错误代码。

SqlClient.SyntaxException相当于我们的ArgumentException。它是指你向服务器传递了糟糕的SQL(直接或者因为ORM的Bug)。

SqlClient.MissingObjectException会在语法正确但数据库对象(表、视图、存储过程等)不存在时出现。

SqlClient.DeadlockException出现在两个或多个进程试图修改相同的信息产生冲突时。

这些异常中的每一种都隐含着一个行动方案。

  • SqlClient.NetworkException:重试操作。如果频繁出现,则请联系运维人员。

  • SqlClient.InternalException:立即联系DBA。

  • SqlClient.SyntaxException:通知应用程序或数据库开发人员。

  • SqlClient.MissingObjectException:请运维人员检查上一次数据库部署是否丢了东西。

  • SqlClient.DeadlockException:重试操作。如果频繁发生,则查找设计错误。

如果要在实际的工作中这样做,那么我们必须将所有11000多个SQL Server错误代码映射到那些类别中的一个,这是一项特别令人望而生畏的工作,这也就解释了为什么SqlException是现在这个样子。


总结

当设计API时,为了便于纠正问题,要将异常根据需要执行的动作的类型进行组织。这样更容易编写出自校代码,记录更准确的的日志,更快地将问题传达给合适的人或团队。


关于作者

Jonathan Allen在90年代末开始参与面向医务室的MIS项目,把它们从Access和Excel逐步提升为一种企业级的解决方案。他花了五年时间编写金融行业自动交易系统,然后决定转向高端用户界面开发。在业余时间里,他喜欢学习15到17世纪之间的西方格斗技巧,并进行相关写作。


原文地址:http://www.infoq.com/cn/articles/Exceptions-API-Design


.NET社区新闻,深度好文,微信中搜索dotNET跨平台或扫描二维码关注

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

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

相关文章

java 百度经纬度 转换为 高德经纬度 谷歌

https://lbs.amap.com/api/webservice/guide/api/convert/#convert https://restapi.amap.com/v3/assistant/coordinate/convert?locations116.481499,39.990475&coordsysgps&outputxml&key<用户的key> https://console.amap.com/dev/key/app https://conso…

方法的重写VS重载

方法的重写&#xff08;override&#xff09; 方法的重写 子类从父类中继承方法&#xff0c;有时&#xff0c;子类需要修改父类中定义的方法的实现&#xff0c;这称做方法的重写(method overriding)。“重写”的概念与“重载”相似&#xff0c;它们均是Java“多态”的技术之一&…

权衡微服务

很多开发团队已经认识到 微服务架构比单体架构更优越。但是也有其他团队感觉到这是一种消弱生产力的负担。就像任何软件架构&#xff0c;微服务架构同样有利弊。为了能做出一个明智的选择&#xff0c;你必须了解这些应用并将它们运用到你特定的环境中。 微服务的优势 具有边界的…

使用IDM下载,不适用默认浏览器下载

google浏览器是不能够安装的除非有v*p*n&#xff08;&#xff09; Firefox浏览器可以安装js脚本 推荐使用 1 google 扩展 IDMan628 扩展程序里面加 IDMGCExt.crx 2 扩展Tampermonkey 加Tampermonkey.crx 3 点击Tampermonkey 的仪表盘 右上角加按钮 百度网盘直接下载助手…

ASP.NET Core 中的那些认证中间件及一些重要知识点

前言 在读这篇文章之间&#xff0c;建议先看一下我的 ASP.NET Core 之 Identity 入门系列&#xff08;ASP.NET Core 之 Identity 入门&#xff08;一&#xff09;&#xff0c;ASP.NET Core 之 Identity 入门&#xff08;二&#xff09;&#xff0c;ASP.NET Core 之 Identity 入…

mybatis-plus 错误java.lang.NoClassDefFoundError: org/apache/velocity/context/Context

https://blog.csdn.net/qq_39609151/article/details/82855305 mybatis-plus 错误java.lang.NoClassDefFoundError: org/apache/velocity/context/Context Murphy_fly 2018-09-26 16:41:55 27168 收藏 15 分类专栏&#xff1a; 框架 版权 使用mybatis-plus自动生成文件的时…

向上类型转换VS向下类型转换

子类转换成父类时的规则: 将一个父类的引用指向一个子类的对象&#xff0c;称为向上转型(upcasting)&#xff0c;自动进行类型转换。此时通过父类引用调用的方法是子类覆盖或继承父类的方法&#xff0c;不是父类的方法。 此时通过父类引用变量无法调用子类特有的方法&#xff…

.NET 使用 RabbitMQ 图文简介

前言 最近项目要使用RabbitMQ&#xff0c;园里里面已经有很多优秀的文章&#xff0c;Rabbitmq官网也有.net实例。这里我尝试下图文并茂之形式记录下使用的过程。 安装 RabbitMQ是建立在erlang OTP平台下&#xff0c;因此在windows下需要下载并安装以下两个组件&#xff1a; 1. …

Springboot+MyBatis-plus+postgresSQL 的整合

https://blog.csdn.net/xuxiannian/article/details/99625085 SpringbootMyBatis-pluspostgresSQL 的整合 禛陌 2019-08-15 12:20:10 6752 收藏 7 分类专栏&#xff1a; 技术相关 文章标签&#xff1a; SpringbootMyBatis-pluspostgresSQL 版权 磨叨一下 MyBatis-plus 请…

使用Senparc.Weixin.WxOpen开发高可用的微信小程序

Senparc.Weixin SDK介绍 Senparc.Weixin SDk是目前.net平台上使用率最高的微信SDK&#xff0c;除硬件平台暂未发布以外覆盖了所有微信平台模块&#xff0c;自2013年免费开源起已经持续更新了4年&#xff0c;是GitHub上目前Star和Fork数最多的中国C#开源项目。 目前大多数模块都…

Java中的事务——JDBC事务和JTA事务

转载自 Java中的事务——JDBC事务和JTA事务 我的博客中曾经关于事务有过很多讨论&#xff0c;之前的事务介绍基本都是数据库层面的事务&#xff0c;本文来介绍一下J2EE中和事务相关的内容&#xff0c;在阅读本文之前&#xff0c;希望读者对分布式有一定的了解。 关于事务的基础…

IDEA使用笔记(八)——自动生成 serialVersionUID 的设置

Ihttps://www.cnblogs.com/godtrue/p/7674487.html https://www.cnblogs.com/godtrue/p/7674487.html DEA使用笔记&#xff08;八&#xff09;——自动生成 serialVersionUID 的设置 这个设置比较简单&#xff0c;也有一些博文已经写到了&#xff0c;为什么我还要写哪&#…

从事件和DDD入手来构建微服务

领域驱动设计&#xff08;Domain-Driven Design&#xff0c;DDD&#xff09;是一项很伟大的技术&#xff0c;它拉近了设计与程序实际所服务的领域&#xff0c;但是通常我们会关注结构&#xff0c;从而太早地做出决策&#xff0c;这并非DDD的本意。相反&#xff0c;在领域中&…

JTA 深度历险 - 原理与实现

转载自 JTA 深度历险 - 原理与实现 利用 JTA 处理事务 什么是事务处理 事务是计算机应用中不可或缺的组件模型&#xff0c;它保证了用户操作的原子性 ( Atomicity )、一致性 ( Consistency )、隔离性 ( Isolation ) 和持久性 ( Durabilily )。关于事务最经典的示例莫过于信…

为了支持AOP的编程模式,我为.NET Core写了一个轻量级的Interception框架[开源]

ASP.NET Core具有一个以ServiceCollection和ServiceProvider为核心的依赖注入框架&#xff0c;虽然这只是一个很轻量级的框架&#xff0c;但是在大部分情况下能够满足我们的需要。不过我觉得它最缺乏的是针对AOP的支持&#xff0c;虽然这个依赖注入框架提供了扩展点使我们可以很…

装箱VS拆箱

我们一般将“基本数据类型转换成包装类”的过程叫做装箱&#xff0c;将“包装类转换成基本数据类型”的过程叫做拆箱。 装箱可以分为手动装箱和自动装箱&#xff1a; 拆箱也可以分为手动拆箱和自动拆箱&#xff1a;

事务模型与分布式事务总结思考

转载自 事务模型与分布式事务总结思考 1. 介绍 之前了解过一些分布式事务处理的思想&#xff0c;包括MVCC、TCC等。但是对具体实现的规范和约束还不够理解清晰。本文从事务模型分类来讨论常见的事务模型。事务模型的含义&#xff0c;应该指的是我们如何去使用可控制事务。 首…

我的这10年——从机械绘图 到 炼油 到 微软MVP 的华丽转身

年底了&#xff0c;各种总结计划满天飞&#xff0c;有空的时候我也一直在思考这么多年&#xff0c;是怎么过来的。也曾经很迷茫&#xff0c;希望经验和经历能给大家一点带来一点正能量的东西。10年很长&#xff0c;10年前说实话我没有思考过现在的样子&#xff0c;但10年前的日…