时间:2016-12-5来源:本站原创作者:佚名

C#6.0新增了两种异常处理功能。首先,它支持异常条件,即能提供表达式通过在堆栈展开之前进入catch块,筛选出异常。其次,它在catch块内添加了异步支持。在将异步添加到C#5.0语言时,这是无法实现的。此外,之前五版C#和相应的.NETFramework中也有其他许多变更,在某些情况下这些变更非常重要,需要对C#编码指南进行编辑。在本期内容中,我将回顾许多变更,并提供更新后的编码指南,因为这些指南与异常处理(即捕获异常)相关。

捕获异常:回顾

很好理解的是,引发特定的异常类型可以让捕获程序使用异常类型本身来确定问题。换言之,其实没有必要捕获异常,也没有必要通过对异常消息使用switch语句来确定采取什么措施处理异常。相反,C#支持多个catch块,每个catch块都能定位特定的异常类型,如图1所示。

图1:捕获不同的异常类型

usingSystem;publicsealedclassProgram{publicstaticvoidMain(string[]args)try{//...thrownewInvalidOperationException(Arbitraryexception);//...}catch(System.Web.HttpExceptionexception)when(exception.GetHttpCode()==){//HandleSystem.Web.HttpExceptionwhe//exception.GetHttpCode()is.}catch(InvalidOperationExceptionexception){boolexceptionHandled=false;//HandleInvalidOperationException//...if(!exceptionHandled)//InC#6.0,placethiswithanexceptioncondition{throw;}}finally{//Handleanycleanupcodeheasitruns//gardlessofwhethertheisanexception}}}

当异常发生时,执行会跳至可以处理此异常的第一个catch块。如果有多个catch块与try相关联,则匹配接近程度依继承链而定(假设不含C#6.0异常条件),且首个匹配项将处理异常。例如,即使引发的异常具有类型System.Exception,这也是“一种”继承关系,因为System.Invalid-OperationException最终源自System.Exception。由于InvalidOperationException最接近匹配引发的异常,因此是catch(InvalidOperationException...)会捕获异常,而不是catch(Exception...)块(如果有的话)。

catch块必须按从最具体到最笼统的顺序显示(同样假设不含C#6.0异常条件),以免出现编译时错误。例如,将catch(Exception...)块添加到其他所有异常之前会导致编译错误,因为之前的所有异常都源自继承链上某处的System.Exception。另请注意,catch块不要求使用命名参数。实际上,最终捕获即使没有参数类型也是允许的,不过这只限常规catch块。

有时,在捕获异常后,您可能会发现实际上无法充分处理异常。在这种情况下,您主要有两种选择。第一种选择是重新引发其他异常。在以下三种常见方案中,您可以这样做:

方案1:捕获的异常无法充分确定异常触发问题。例如,当使用有效URL调用System.Net.WebClient.DownloadString时,运行时可能会在没有网络连接的情况下引发System.Net.WebException,不存在的URL也会引发同种异常。

方案:捕获的异常包含不得在调用链前端公开的专用数据。例如,很早以前的CLRv1版本(甚至是初期测试版)有诸如“安全异常:您无权确定c:\temp\foo.txt的路径”之类的异常。

方案3:异常类型过于具体,以至于调用方无法处理。例如,当调用Web服务查找邮政编码时,服务器发生System.IO异常(如Unauthorized-AccessException、IOException、FileNotFoundException、DictoryNotFoundException、PathTooLongException、NotSupportedException、SecurityException或ArgumentException)。

重新引发其他异常时,请注意,您可能会丢失原始异常(可能就会发生方案中的情况)。为了避免这种情况,请使用已捕获的异常设置包装异常的InnerException属性,通常可以通过构造函数进行分配,除非这样做会公开不得在调用链前端公开的专用数据。这样一来,原始堆栈跟踪仍可用。

如果您不设置内部异常,但仍在throw语句(引发异常)后面指定异常实例,则异常实例上会设置位置堆栈跟踪。即使您重新引发之前捕获的异常(已设置堆栈跟踪),系统也会进行重置。

第二种选择是在捕获异常时,确定您实际上是否无法适当处理异常。在这种情况下,您需要重新引发完全相同的异常,并将它发送给调用链前端的下一个处理程序。图1的InvalidOperationExceptioncatch块展示的就是这种情况。throw语句没有确定要引发的异常(完全依靠自身引发),即使异常实例(异常)出现在可以重新引发的catch块范围内,也是如此。引发特定的异常会将所有堆栈信息更新为匹配新的引发位置。结果就是,所有指明调用站点(即异常的最初发生位置)的堆栈信息都会丢失,这会导致问题更加难以诊断。在确定catch块无法充分处理异常后,应使用空的throw语句重新引发异常。

无论您是要重新引发相同的异常,还是要包装异常,常规指南是避免在调用堆栈的下端报告或记录异常。换言之,不要每次捕获和重新引发异常都进行记录。这样做会在日志文件中造成不必要的混乱,并且也不会增加价值,因为每次记录的内容都相同。此外,异常还包含引发异常时的堆栈跟踪数据,所以无需每次都进行记录。请务必记录处理的异常,或者在不处理的情况下,在关闭进程之前,对异常进行记录。

在不替换堆栈信息的情况下引发现有异常

C#5.0中新增了一种机制,可以在不丢失原始异常中的堆栈跟踪信息的情况下,引发之前已引发的异常。这样,您便可以重新引发异常(例如,从catch块外部引发),因此无需使用空的throw。尽管需要这样做的情况很少,但有时在程序执行移至catch块外部之前,异常可能已包装或保存。例如,多线程代码可能使用AgggateException包装异常。.NETFramework4.5提供了专门用于处理这种情况的System.Runtime.ExceptionServices.ExceptionDispatchInfo类,它是通过使用静态Captu和实例Throw方法。图展示了如何在不重置堆栈跟踪信息或不使用空的throw语句的情况下,重新引发异常。

图:使用ExceptionDispatchInfo重新引发异常

usingSystemusingSystem.Runtime.ExceptionServices;usingSystem.Thading.Tasks;Tasktask=WriteWebRequestSizeAsync(url);try{while(!task.Wait()){Console.Write(.);}}catch(AgggateExceptionexception){exception=exception.Flatten();ExceptionDispatchInfo.Captu(exception.InnerException).Throw();}

借助ExeptionDispatchInfo.Throw方法,编译器不会将它看作turn语句,就像是对正常的throw语句一样。例如,如果方法签名返回了值,但使用ExceptionDispatchInfo.Throw没有从代码路径返回任何值,则编译器会发出错误来指明没有值返回。有时,开发者可能不得不遵循含turn语句的ExceptionDispatchInfo.Throw,即使在运行时此类语句从不执行,而是会引发异常,也是如此。

在C#6.0中捕获异常

常规的异常处理指南是避免捕获您无法完全处理的异常。然而,由于C#6.0之前的捕获表达式只能按异常类型进行筛选,因此在检查异常之前,catch块必须是异常的处理程序,才能够在堆栈展开之前,在catch块处检查异常数据和上下文。可惜的是,在决定不处理异常后,编写代码以便相同上下文内的不同catch块能够处理异常是一项很繁琐的做法。此外,重新引发相同的异常会导致不得不再次调用双步异常进程。此进程涉及的第一步是在调用链前端提供异常,直至发现可处理异常的对象;涉及的第二步是为在异常和catch位置之间的每个框架展开调用堆栈。

引发异常后,与其因为进一步检查异常后发现无法充分处理异常,而在catch块处展开调用堆栈,只是为了重新引发异常,不要一开始就捕获异常明显是更可取的做法。对于C#6.0及更高版本,catch块可以使用额外的条件表达式。C#6.0支持条件子句,不再限制catch块是否只能根据异常类型进行匹配。借助when子句,您可以提供布尔表达式进一步筛选catch块,仅在条件为true时处理异常。图1中的System.Web.HttpException块通过相等比较运算符展示了这一功能。

使用异常条件的有趣结果是,当有异常条件时,编译器不会强制catch块按继承链中的顺序显示。例如,附带异常条件的System.ArgumentException类型catch现在可以显示在更具体的System.ArgumentNullException类型之前,即使后者源自前者,也是如此。这一点非常重要,因为这样您便可以编写与常规异常类型(后面是更具体的异常类型,带有或不带异常条件)配对的具体异常条件。运行时行为仍然与早期版本的C#保持一致;异常由首个匹配的catch块捕获。增加的复杂性仅仅是,catch块是否匹配由类型和异常条件的组合决定,并且编译器只会强制实施与不带异常条件的catch块相关的顺序。例如,带有异常条件的catch(System.Exception)可以显示在带有或不带异常条件的catch(System.Argument-Exception)之前。然而,在不带异常条件的异常类型的catch显示后,不可能再出现更具体的异常catch块(如catch(System.ArgumentNullException)),无论其是否带有异常条件。这样一来,程序员可以“灵活地”对可能乱序的异常条件进行编码,早期的异常条件可以捕获为后面的异常条件而设的异常,甚至可以呈现无意中无法访问的后期异常。最终,catch块的顺序与if-else语句的顺序相似。在条件符合后,系统会忽略其他所有catch块。然而,与if-else语句中的条件不同的是,所有的catch块都必须包含异常类型检查。

更新后的异常处理指南

虽然图1中的比较运算符示例非常容易,但异常条件并不只是简单而已。例如,您可以进行方法调用来验证条件。唯一的要求是表达式必须是谓词,可以返回布尔值。换言之,您基本上可以在catch异常调用链内部执行所需的任何代码。这样一来,您就有机会再也不捕获和重新引发相同的异常;从根本上讲,您可以在捕获异常前,充分地缩小上下文的范围,这样就可以仅在这样做有效时才捕获异常。因此,避免捕获您无法完全处理的异常这一指南就可以真正落实。实际上,任何有关空的throw语句的条件检查都可以用代码进行标记,并且是可以避免的。请考虑添加异常条件,支持使用空的throw语句,在进程终止前保持可变的状态除外。

也就是说,开发者应该将条件子句限制为只检查上下文。这一点非常重要,因为如果条件表达式本身引发异常,则新的异常会遭到忽略,并且条件会被视为false。因此,您应该避免在异常条件表达式中引发异常。

常规catch块

C#要求代码引发的所有对象都必须源自System.Exception。然而,此要求并不通用于所有语言。例如,C/C++允许引发任何对象类型,包括不是源自System.Exception的托管异常或基元类型(如整数或字符串)。对于C#.0及更高版本,所有异常都会作为源自System.Exception的异常传播到C#程序集中,无论异常是否源自System.Exception。结果就是,System.Exceptioncatch块会捕获所有未被之前的catch块捕获的“合理处理”异常。然而,在C#1.0之前,如果通过方法调用(驻留在程序集中,而不是在C#中编写)引发非源自System.Exception的异常,则catch(System.Exception)块不会捕获异常。因此,C#也支持行为现在与catch(System.Exceptionexception)块完全相同的常规catch块(catch{}),除非没有类型或变量名称。此类块的缺点就是,没有可访问的异常实例,因此没有办法了解相应的行动措施。甚至无法记录异常或确定并不多见的情形(即此类异常无关紧要)。

在实践中,catch(System.Exception)块和常规catch块(本文通常称为catchSystem.Exception块)都是可以避免的,只需在关闭进程前记录异常即可,“处理”异常的幌子除外。遵循只捕获您可以处理的异常这一基本原则,而编写程序员声明的代码似乎很冒失(此catch可以处理所有可能引发的异常)。首先,登记所有异常(特别是在Main主体中,其中执行代码的量是最多的,而且上下文的量似乎是最少的)的工作量似乎非常巨大,最简单的程序除外。其次,有许多可能意外引发的异常。

在C#4.0之前,程序通常无法恢复第三组的损坏状态异常。然而,对于C#4.0及更高版本,这个组就不太受到







































北京白癜风哪个医院治得好
皮肤病治疗最好医院

转载请注明原文网址:http://www.helimiaopu.com/cxkf/2490.html

------分隔线----------------------------