车公庄附近金手勺饭店:.Net的异常处理

来源:百度文库 编辑:偶看新闻 时间:2024/05/09 10:40:38

与使用返回值来报告错误相比,.NET的异常处理有许多优势

  • 异常与面向对象语言有效地集成使得在无法使用返回值的时候可以使用异常。
  • 异常增强了API的一致性,设计异常就是为了报告错误,而返回值则有多种用途。
  • 异常使错误处理更加灵活。在使用返回值报告错误时,处理代码总是与可能发生错误的代码距离很近,但是使用异常处理时,异常可以被传递,从而在调用栈上游来处理。
  • 异常有助于降低编码的复杂度。使用返回值往往伴随多组if语句,如果使用异常则只要一组try...catch...就能解决。
  • 异常帮助你发现缺陷有助于检测。你可能会忽略一个错误代码,但不会忽略一个异常,任何调试工具都会时刻注意异常的发生。
  • 异常包含了更丰富的信息,我们再不需要那些模糊的数字来定义问题了。异常包含了易理解的错误描述和首次触法异常时调用栈的详细快照。
  • 异常允许用户定义未处理异常的处理程序。在发生意料之外的失败时,系统最终会调用未处理异常的处理程序,该程序可以将失败记录下来也可以选择关闭程序。

 .Net异常处理的四要素

  1. 一个表示异常详细信息的类。
  2. 一个像调用者引发异常类实例的成员。
  3. 调用者的一段调用异常成员的的模块。
  4. 调用者的一段处理或捕获将要发生异常的代码块。

System.Exception类与系统级异常System.SystemException类

所有用户定义和系统定义的异常最终都继承自System.Exception类,其核心成员如下:

 名称说明Data获取一个提供用户定义的其他异常信息的键/值对的集合。HelpLink获取或设置指向此异常所关联帮助文件的链接。 InnerException获取导致当前异常的 Exception 实例。 Message获取描述当前异常的消息。Source获取或设置导致错误的应用程序或对象的名称。 StackTrace获取当前异常发生时调用堆栈上的帧的字符串表示形式。TargetSite获取引发当前异常的方法。

.Net平台引发的异常称为系统异常,被认为是无法修复的致命错误。系统异常直接派生自System.SystemException类。当一个异常类派生自System.SystemException时,我们就可以判断引发该异常的实体是.Net运行库而不是正在执行的应用程序代码库。不要抛出System.Exception或System.SystemException异常,除非打算重新抛出或是在顶层的异常处理其中才考虑捕获System.Exception或System.SystemException异常。

应用程序级异常System.ApplicationException类

System.ApplicationException的唯一目的就是标识出错误的来源,其最初的想法是用派生自ApplicationException的类来表示非.Net运行库抛出的异常。但是悲剧了,微软开发时很多异常类都没有遵守这一模式。现在想用ApplicationException捕获所有应用程序异常已经是不可能了。所以不要抛出ApplicationException,也不要从它派生新类。

用户可以显式抛出的异常

  • InvalidOperationException 类

  当方法调用对于对象的当前状态无效时引发的异常。该异常我们可以显式抛出。如果调用方法失败不是由无效参数造成的,则使用 InvalidOperationException。

  • ArgumentException 、ArgumentNullException 、ArgumentOutOfRangeException

  要在用户传入无效参数时抛出ArgumentException或其派生类ArgumentNullException 或ArgumentOutOfRangeException。

  要在抛出它们时设置ParamName属性。例如:

throw new ArgumentNullException("path",...);

  要在属性的Setter中,以value作为value隐式参数的名字。例如:

public FlieAttribute Attributes
{
set
{
if(value==null)
{
throw new ArgumentNullException("value",...);
}
}
}
复制代码

  除了这里提到的4个异常外,其它异常都是为CLR服务的,大多数情况下他们表示代码存在缺陷,用户请不要显式地抛出。

自定义异常

原则上优先使用InvalidOperationException、ArgumentException 、ArgumentNullException 和ArgumentOutOfRangeException,这4个异常类型,用户可以显示抛出。当这些异常无法满足需要时,比如某个异常的处理方式相对于其他异常的处理方式不同,或者要传达独一无二的错误信息则需要自定义异常。

自定义异常的设计规范如下:

  • 要从System.Exception或其它常用的异常基类派生新的异常。
  • 避免太深的继承层次。(异常处理一次只处理一个错误,基本不关心异常的继承层次)
  • 使用“Eception”命名。
  • 要使异常可序列化。(跨应用程序域和远程边界)
  • 要为所有异常(至少)提供下面这些常用的构造函数。下面会给出异常处理的最佳实践。
  • 提供一个ToString方法的覆盖。(不包含安全有关信息)
  • 要把安全性有关的信息保存在私有的异常状态中,并确保可信赖的代码才能得到该信息。
  • 考虑为异常定义属性。(得到消息字符串之外的信息)

异常处理的最佳实践,自定义异常需要:

  • 继承自Exception/ApplicationException类。
  • 有[System.Serializable]特性标记。
  • 定义一个默认的构造函数。
  • 定义一个设定继承的Message属性的构造函数。
  • 定义一个处理“内部异常”的构造函数。
  • 定义一个处理系列化的构造函数。
[Serializable]
public class MyException:ApplicationException
{
public MyException(){}
public MyException(string message):base(message){}
public MyException(string message,System.Exception inner):base(message,inner){}
protected MyException(SerializationInfo info,StreamingContext context):base(info,context){}
...
}
复制代码

错误消息的设计

  • 要在抛出异常时提供丰富而有意义的错误消息,要明确你的消息的目标人群到底是最终用户还是开发人员。(多数异常消息是给开发人员看的,因为修正问题的唯一方法是错误报告、修改代码发布新版本。)
  • 要确保异常中的消息语法正确无误。
  • 要确保消息中每个句子都有句号。
  • 避免在异常消息中使用问号或惊叹号。
  • 不要在异常消息中泄露安全信息。
  • 考虑把组件抛出的异常消息本地化。

封装异常

考虑对底层异常进行适当的封装——如果底层异常在高层的运行环境中没有什么意义。原则上,如果用户想要查看内部异常,就不要对其进行封装。(封装可能会影响可调试性)

try
{
...//read the transaction file
}
catch(FileNotFoundException ex)
{
       //必须为其指定内部异常
throw new TransactionFileMissingException (...,ex);
}
复制代码

避免封装类型不确定的异常。这是吞掉(在不了解失败原因的情况下或没有对失败做出反应的情况下,让程序继续运行)错误的一种形式,通常是为了表示极其严重的错误,其重要性已经超出了让调用者知道原来异常的具体类型,例如TypeInitializationException对静态构造函数中引发的异常进行了封装。

抛出异常要注意的问题

  • 考虑在代码遇到了严重问题且无法继续安全执行时,通过调用System.Environment.FailFast来终止进程,而不是抛出异常。
  • 请尽量不要在正常的控制流中使用异常。在编写API时应该为用户提供一个方法,用来在调用某个成员之前检查前置条件,这样用户编写的代码就不会引发异常。例如:
if(!collection.IsReadOnly){collection.Add(...);}
复制代码
  • 在对性能要求极高的程序中,不应该过多的抛出异常。
  • 要为自定义异常撰写文档。
  • 不要让公有成员根据某个选项来决定是否抛出异常。这是糟糕的设计,API使用者是不知道你的实现细节的,你不能让使用者来做决定。例如:
public SomeType Do(string str,bool throwOnError)
复制代码
  • 不要把异常用做返回值或输出参数。异常要“抛出”,不要“返回”。
  • 考虑使用辅助方法来创建异常。这样的好处是避免因为抛出一个异常而带来的代码重复,另外,抛出异常的成员无法被内联,如果把抛出异常的语句移到辅助函数里,那么该成员就由肯能被内联。辅助函数例如:
void throwMyException(...)   
{
do some work...
string desc=...
throw new MyException(desc);
}
复制代码
  •  避免显示地在finally块中抛出异常。

异常处理

程序中有大量代码在做异常处理工作,下面是在设计异常处理时应该考虑的问题:

1 异常处理是进行纠正还是仅仅进行检查?

2 错误检测是主动的预防还是被动的捕获?

3 程序如何传递异常?(捕获异常后:a直接丢弃数据,b进入异常处理的后继操作,c记入日志并继续后继操作,d其它...)

4 异常消息的处理有什么约定?

5 如何处理异常?(何时能够抛出异常?在什么地方捕获异常?如何记录异常?如何描述异常?)

6 在哪层处理异常,异常是否会传递?

7 类在验证输入数据有效性方面要负何种责任?(安全限界)

8 你希望运用运行环境中内建的错误处理机制还是希望自定义?

错误处理技术:

  • 返回中立值(值类型返回0,引用类型返回null。)
  • 换用下一个正确的数据
  • 返回与前次相同的数据
  • 换用最接近的合法数据(比如,当值低于下限时,返回下限值。)
  • 把警告信息记入到日志中
  • 抛出一个异常
  • 调用全局错误处理程序(优点是把错误处理职责集中到一起,从而使调试工作更为简单;缺点是,整个程序都要知道这个集中点并与之紧密耦合。)
  • 当错误发生时显示出错信息(特别要注意不要显示与安全有关的信息。)
  • 用最妥当的方式在局部处理错误(可能导致与UI有关的代码散布到整个系统中。)
  • 关闭程序

异常处理应该遵循的原则:

  • 不要毫无意义地捕获类型不确定的异常。如果捕获这类异常,却不做妥善处理,而放任程序继续运行,往往会引发奇奇怪怪的错误。
  • 要在进行清理工作时使用try-finally,避免使用tty-catch,在清理实现IDisposable接口的对象时,推荐使用using。
  • 在捕获并重新抛出异常时使用空的throw语句,这是保证异常调用栈不变的最好方法。
  • 用不带参数的Catch块处理其他不符合CLS规范(没有派生自System.Exception)的异常。

public void Do(FileStream file)
{
long position=file.position;
try
{
...
}
catch//通用Catch块,不显式接收指定异常,虽然无法获取关于错误的有益信息,但可以用来处理所有错误。
{
file.position=position;
throw;//再次引发异常,这样做保留了原始对象的上下文。
}
}
复制代码

处理多个异常

try
{
...//该异常引发后将被第一个可用的Catch块处理。
}
catch(MyException ex)
{
...//最前面的Catch块捕获最特定的异常(派生类排在继承关系最后的派生类型派在最前面)。
}
catch(Exception ex)
{
...//后面的Catch捕获最普遍的异常(异常派生链中的基类)。
}

finally
{
...
}
复制代码

 嵌套的try块

try
{
...//a 在此抛出AException,外层异常由外层catch处理,此时不会进入内层try
try
{
...//b 在此抛出异常BException
}
catch
{
...//c 尝试捕获BException,如果类型合适,则在内层处理异常,执行内层finally,之后继续执行d处代码
            //c 无法捕获BException,执行内层finally,到外层catch尝试捕获异常,不会执行d处代码
            //c 在此抛出异常CException,立即退出内层catch块,执行内层finally,到外层catch尝试捕获异常,不会执行d处代码

}
finally
{
...//e 执行内层finally
           //e 在此抛出异常EException,立即退出内层finally块,到外层catch尝试捕获异常,不会执行d处代码
}
...//d 在此抛出DException,外层异常由外层catch处理

}
catch
{
...//尝试捕获外层异常,及内层未处理的异常,不管成功与否都将执行外层finally,捕获失败时如果没有更多的catch则将控制权返还给.Net运行库



}
finally
{
...//执行外层的finally
}
复制代码

 嵌套try块有2个作用:

  1. 修改所抛出的异常类型(包装异常)。
  2. 在不同地方处理不同异常。例如,在循环中,用一个内部try处理不太严重的异常,只需要退出这次迭代,进入循环的下一次迭代即可;在循环体外面用一个外层的try处理比较严重的错误,需要退出整个循环。

异常性能优化

成员抛出异常时,对性能产生的影响是指数级的。以下两种模式有助于提高性能。

Tester-Doer模式 其代码形如:

ICollection numbers=...
if(!numbers.IsReadOnly)
{
numbers.Add(1);
}
复制代码

在设计具备线程安全性的类时,要小心使用这种模式。如果多个线程同时访问一个对象,某个线程可能会运行Tester方法并通过测试,但在doer方法运行之前,另一个线程可能会改变对象的状态并导致Doer的操作失败。这个模式引入了竞态条件,在使用时如果你的类不具备线程安全性,那么必须使用所来确保同一时间只有一个线程使用它。只要用一个锁来锁定Tester和Doer就不会出现问题。

Try-Parse模式 其代码形如:

public struct DateTime
{
public static DateTime Parse(string dateTime){...}
public static bool TryParse(string dateTime,out DataTime result){...}
}
复制代码

与Tester-Doer相比,此模式的性能更高。要注意严格定义try操作,如果因为try操作失败的原因而导致成员失败,那么成员仍应抛出异常。建议使用与.Net一致的风格,即使用Try前缀,并有bool类型作为返回值;为每个使用Try-Parse模式的方法提供一个会抛出异常的对应成员。