【.com原稿】.NET中的异常处理是一个庞大的模块,专门用来处理程序中已知的可捕获异常。在这篇文章中,我将详细讲解异常处理的细节,包括异常处理类型、自定义异常处理、multi-catch异常处理以及异常处理依赖。1、异常处理类型C#允许我们编写抛出任何派生自System.Exception的异常类型的代码(包括间接派生和直接派生)。例如下面的代码段:publicclassDemo{publicintStringToNumber(stringpara){string[]numberArray={"zero","one","two","three"};intnumber=Array.IndexOf(numberArray,(para??thrownewArgumentNullException(nameof(para))));if(number<0){thrownewArgumentException("参数值无法转换为数字",nameof(para));}returnnumber;}}以上代码使用throw关键字抛出异常,并使用特定的异常类型来描述异常发生的上下文。在代码中,我们只使用了C#7.0的新特性throw表达式。当para为null时,将抛出ArgumentNullException。当number的值小于0时,我们不会抛出Exception类型的异常,而是会抛出ArgumentException类型的异常,可以更明确的告知异常原因。我们从代码中可以看出,当para参数为null时,抛出的是ArgumentNullException类型的异常,而不是NullReferenceException类型的异常。对于这两类异常,很多开发者其实并不清楚它们之间的区别。其实两者的区别很简单。错误传递空值时会抛出ArgumentNullException。如果传递了非空的无效参数,则必须使用ArgumentException或ArgumentOutOfRangeException。如果底层运行时发现对象的值为null,则会抛出NullReferenceException类型的异常。一般来说,开发者不能随意抛出这个异常。我们在使用参数之前应该先判断参数是否为null。如果为null,则抛出ArgumentNullException。除了NullReferenceException异常,还有五个派生自System.SystemException的异常是不能自己抛出的,只能在运行时抛出。它们是System.StackOverflowException、System.OutOfMemoryException、System.Runtime.InteropServices.COMException、System.ExecutionEngineException和System.Runtime.InteropServices.SEHException。同样,开发者尽量不要在程序代码中抛出Exception和ApplicationException,因为它们反映的异常过于笼统,无法为异常提供清晰的信息。在实际项目开发中,当代码执行到一定程度时,可能会遇到不安全或不可恢复的状态。这时候大部分情况下代码是不会出现异常的,所以我们必须调用System.Environemnt.FailFast方法终止程序。该方法在向练习日志写入消息后立即终止程序进程。在前面的代码中,我们还使用了nameof运算符。使用此运算符的第一个原因是我们可以使用重构工具方便地自动更改标识符。此外,如果参数名称发生变化,我们可以及时收到编译错误。对本节内容做一个简单的总结:当一个成员接收到一个不正确的参数时,应该抛出一个ArgumentException或其子类型异常;抛出ArgumentException或子类型异常时,必须设置ParamName属性,即nameof;抛出的异常必须明确说明异常的问题;避免意外获取空值时抛出NullReferenceException;不要抛出System.SystemException及其派生的异常;不要抛出Exception和ApplicationException;如果程序没有出于安全考虑,必须调用System.Environemnt.FailFast方法终止程序的运行;对传递给参数异常类型的ParamName使用nameof运算符提示:参数异常类型包括ArgumentNullException、ArgumentNullException、ArgumentOutOfRangeException2.捕获异常处理捕获异常处理第一节比较简单。主要要理解和掌握的是multi-catch块的顺序和异常类型,还有when子句。多个catch块多个catch块在C#中比较常见。我们在上一节中说过,抛出的异常必须能够明确的指出异常问题,所以我们可以使用多个catch块来解决一个代码段中可能出现的多次出现的情况。每个catch块处理一种异常情况。让我们看一个简单的代码段:{//morecode}catch(Exceptionex){//morecode}}在上面的代码中,我们一共定义了5个catch块。当异常发生时,会被相应的catch块拦截处理。这一节就这么简单,主要是讲多个catch块的使用。在下一节中,我将解释catch块中最重要的内容。异常类型的顺序异常类型的顺序是很多初学者甚至一些老程序员都会犯的问题。从前面的代码我们也可以看出,Exception在最后一个位置,而IOException在倒数第二个位置。这是因为Exception是所有异常的父类,所有异常都是直接或间接派生自它的,而IOException是DirectoryNotFoundException和FileNotFoundException的父类。按照异常匹配的先后顺序,C#总会匹配第一个符合要求的异常。如果父类异常放在子类异常的前面,那么当代码中出现异常时,会直接匹配父类异常的catch。匹配以下子类异常捕获。提示:无论如何,Exception必须作为最后一个捕获。当程序中出现不匹配任何catch块的异常时,可以被Exceptioncatch块拦截并处理。When子句从C#6.0开始,catch块支持条件表达式,这样我们就可以匹配程序中发生的异常,而不管异常类型。when子句返回一个布尔值,catch块将在返回true时执行。让我们看一个使用when子句的例子:try{//morecode}catch(Win32Exceptionex)when(ex.NativeErrorCode==42){//morecode}但是我们也可以在catch块中使用if语句来执行上面的代码conditionCheck,但是这样做的话,整个catch块的逻辑先变成了exceptionhandler,然后再进行条件判断,导致如果条件不满足,就无法执行其他满足要求的catch块。如果使用when子句,程序可以先检查条件再决定是否执行catch块。但是当它本身有它自己的警告。如果when子句中抛出异常,新的异常将被忽略,整个when子句的返回值变为false。重新抛出异常这里简单说一下重新抛出异常。有些开发人员喜欢在catch块中写这样的语句throwex。这个说法有个致命的问题。把这个写在catch块中会抛出新的异常,导致所有的栈信息被更新而丢失原来的栈信息,很难定位问题。因此,C#开发组设计了一种不指定具体异常的方法,就是直接在catch块中使用throw语句。这样我们就可以判断当前catch块是否可以处理异常,如果不能,则抛出原来的堆栈信息。3.常规的catchC#要求代码抛出的任何对象都必须从Exception派生。从C#2.0开始,所有的异常,无论是否派生自Exception,在进入程序集后都会被打包为派生自Exception。结果是捕获Exception的catch块现在捕获了前一个块无法捕获的所有异常。BriefC#还支持常规catch块,即catch{},它的行为与catch(Exceptionex)块相同,只是它没有类型和变量名。它还必须位于所有catch块的末尾。如果代码中同时存在常规catch块和catch(Exceptionex)块,编译器将显示警告,因为程序将始终匹配catch(Exceptionex)块而不是常规catch块。C#中之所以有一个常规的catch块,是因为如果程序中调用了其他语言开发的程序集,程序集在使用过程中抛出异常,那么这个异常就不会被捕获(Exceptionex)块被拦截,但进入未处理状态,为了避免这个问题c#引入了一个常规的catch块。提示:虽然常规catch块很强大,但它仍然存在问题。它没有可访问的异常实例,因此无法确定该异常对程序是无害还是有害。原理常规catch生成的CIL代码是catch(object),也就是说无论抛出什么类型都能被捕获。虽然生成的CIL代码是catch(object),但是我们不能直接在代码中写这个。常规的catch块无法捕获非Exception派生的异常,所以C#在设计时统一将所有来自其他语言的异常设置为System.Runtime.InteropServices.SEHException异常,所以常规的catch块可以捕获继承自Exception的异常,并且可以捕获来自非托管代码的异常。4.规范异常处理规范不是微软规定的,而是开发者在上千个项目中总结出来的。下面我们一起来看看吧。只捕获可以处理的异常通常我们只处理当前代码可以处理的异常,不能处理的异常会被抛出让栈中更高层的调用者处理。Don'thideunhandledexceptions这个问题发生在捕获所有异常但不处理或抛出它们的新手身上。在这种情况下,如果系统出现问题,就会逃过检测。少用Exception和常规的catch块所有的异常都是继承自Exception,所以使用Exception来处理异常并不是一个最优的方法,有些异常需要立即关闭程序进程。避免在调用栈的低层报告或记录异常调用栈的低层大部分不能完全处理异常,所以只能抛出异常,如果在这些位置记录异常然后再次抛出,异常将被重复记录。当无法处理异常时,使用throw而不是throwex抛出新的异常将导致堆栈跟踪重置到重新抛出的位置,而不是重新使用原来的抛出位置。因此,如果您不需要重新抛出不同的异常类型,或者如果您不想故意隐藏原始调用堆栈,则应该使用throw以允许相同的异常向上传播调用堆栈。避免在catch块中重新抛出异常如果在开发过程中发现捕获到的异常不能完全或正确处理,需要抛出异常,那么就需要重新优化捕获异常的条件。避免在when子句中抛出异常。在when子句中抛出异常将导致表达式的结果变为假,这将阻止catch块运行。避免以后when子句条件改变这种情况通常是因为本地化导致异常发生改变,那么这就是我们必须改变when子句条件的地方。5.自定义异常处理一般来说,当抛出异常时,我们应该使用c#提供的异常类型。但在某些情况下,我们需要自定义异常。比如我们写的API,被其他语言的开发者调用。这个时候,我们不能用我们使用的语言抛出异常。我们应该自定义异常,让调用者清楚我知道哪里出了问题。自定义异常一般派生自Exception或其他异常类,这是唯一的要求。自定义异常还必须符合以下三个要求:异常名称以Exception结尾;必须包含一个无参构造函数,一个包含唯一参数类型为字符串的构造函数,以及一个同时获取字符串和内部异常作为参数的构造函数;集成级别不能大于5级。有些程序要求异常是可序列化的,这时候我们就可以使用可序列化异常。我们只需要在自定义异常类型上添加System.SerializableAttribute属性或者实现ISerializable,然后添加一个构造函数来获取SerializationInfo和StreamingContext即可。这里需要注意的是,如果你使用的是.NETCore2.0以下的版本,你将无法使用可序列化异常。6.小结本文讲解了C#中的异常处理。这里需要提醒大家,抛出异常会影响程序的性能。它会加载和处理大量额外的运行时堆栈信息,整个过程将花费相当长的时间,所以我们在编写程序时应该尽量避免大量使用抛出异常。作者简介朱刚,笔名苗叔,国内技术博客认证专家,.NET高级开发工程师。7年一线开发经验,参与过电子政务系统和AI客服系统的开发,以及互联网招聘网站的架构设计。目前就职于一家创业公司,从事企业级安全监控系统的开发。【原创稿件,合作网站转载请注明原作者和出处为.com】
