这几天看C#基础书籍中关于异常处理部分的内容,总结一下我的收获,呵呵!
总共有以下几个收获:
- 如何能有一个机制在APP最顶层捕获到所有抛出的异常(包括被捕获的和未被捕获的),而又不影响App内部处理每个异常的方式?
- 捕获异常私了还是将异常简单处理后继续抛出?
- 如何快速高效的定义自己的异常类型?
- 异常和性能
下面详细谈一谈:
捕获应用程序域中抛出的所有异常
在开发过程中,有时候我们可能需要有一个统一的接口来处理所有当前托管代码中任何位置抛出的异常。实际上,由于很多的异常在代码的不同位置被我们自己写的代码已经捕获了,而有些代码又没有被捕获,还有些异常是被捕获之后又重新被抛出了,等等。其实,AppDomain这个对象上有一个很重要的事件,那就是FirstChanceException,这个事件可以在一个异常触发后,而且是在未被其他代码捕获前首先触发该事件,并把异常对象包含在FirstChanceExceptionEventArgs中,换句话说,该事件能够在第一时间向我们报告任何一个被抛出的异常,包括Re-Throw,下面是它的声明:
// // Summary: // Occurs when an exception first occurs in managed code, before the runtime // has made its first pass searching for suitable exception handlers. public event EventHandler<FirstChanceExceptionEventArgs> FirstChanceException;
从注释上已经可以看到,该事件可以让我们尝到最新鲜、最及时的异常。
下面是我写的一个Demo例子,来观察一下它的行为。首先是在main函数中对currentDomain注册FirstChanceException事件的处理方法。
static void Main(string[] args) { AppDomain.CurrentDomain.FirstChanceException += new EventHandler<System.Runtime.ExceptionServices.FirstChanceExceptionEventArgs>
(CurrentDomain_FirstChanceException); SimpleExceptionTest.Test(); Console.ReadLine(); } static void CurrentDomain_FirstChanceException(object sender, System.Runtime.ExceptionServices.FirstChanceExceptionEventArgs e) { Console.WriteLine("CurrentDomain_FirstChanceException:"); Console.WriteLine(e.Exception); Console.WriteLine(); Console.WriteLine(); }
然后我编写了一个可以抛出异常的并能自己捕获的SimpleExceptionTest类:
class SimpleExceptionTest { static void TestInner() { try { Console.WriteLine("Exec in Try block!"); object o = null; Console.WriteLine(o.GetType()); } catch (NullReferenceException e) { Console.WriteLine("Exec in catch block! "); Console.WriteLine(e.Message); Console.WriteLine("Exec end catch block! "); throw; } finally { Console.WriteLine("================Exec in finally block!"); int i = 0; //finnally 块中产生异常 int j = 5 / i; Console.WriteLine("================Exec end in finally block!"); } } public static void Test() { try { TestInner(); } catch (Exception e) { Console.WriteLine(e.Message); } } }
程序执行的结果如下:
Exec in Try block!
CurrentDomain_FirstChanceException:Object reference not set to an instance of an
object.
Exec in catch block!
Object reference not set to an instance of an object.
Exec end catch block!
CurrentDomain_FirstChanceException:Object reference not set to an instance of an
object.
================Exec in finally block!
CurrentDomain_FirstChanceException:Attempted to divide by zero.
Attempted to divide by zero.
从上面的执行结果可以看出如下几个问题:
- CurrentDomain_FirstChanceException总是在第一时间捕获到了异常,包括在Catch块中ReThrow的异常。
- Finnally块中抛出异常时,中断了Finally块的执行。我们知道,在Finally块中,通常是写一些清理性的代码,比如释放Try中开启的资源,关闭Try中启动的等待窗等等。所以如果你的finally中又会抛出异常的话,清理工作就会中断了。
那么如何才能保证Finnally块中的清理工作一定能够正确完成哪,我们难道要在Finally块中嵌套一个TryCatchFinnally语句吗,这将会形成死循环的,最后一个Finnally总是没有保证的。唯一可行的办法就是在finnally中不要放置多余的代码,并且认真的写出高质量的清理代码,以祈求它不要再抛出异常。
捕获异常私了还是将异常简单处理后继续抛出
这个问题也是很多初学者的疑惑。我认为有以下几个原则:
1、只私了自己可以明确知道如何处理的异常,比如 从业务和项目组的约定来看,当除以0发生时,我们都将该表达式的值返回0。 那么这就是一个处理异常的法规,依据它就可以将这个异常私了,使异常止步于此,不再向调用堆栈上层抛出。
2、你是一个独立的模块,而且有需要要求你必须做到决定的健壮,那么你可以捕获所有的异常,能处理的依照法规私了,不能处理的就记录或者将异常以友好的方式报告给用户或者服务器,但是程序并不会因此而崩溃。
3、不知道如何处理的异常,自己捕获到后,进行简单处理(比如将异常包装一下,加上更加明确的异常出现位置啊,异常触发时的业务参数信息等)再抛出,交给调用堆栈的上层去捕获后处理。
如何快速高效的定义自己的异常类型
自定义一个异常,其实挺简单的,只要定义一个类并继承自System.Exception或其子类即可。但是那会造成异常类型繁多冗杂,我们自己的团队成员都会记不清楚该有多少异常需要捕获了。而且定义一个符合业务需要的异常又谈何容易。
下面推荐一个CLR via C#一书中定义的异常泛型类Exception<TExceptionArgs> ,只要定义一个新的TExceptionArgs,就能可以让我们获得一个实用的自定义异常类型。
[Serializable] public sealed class Exception<TExceptionArgs> : Exception, ISerializable where TExceptionArgs : ExceptionArgs { private const String c_args = "Args"; // For (de)serialization private readonly TExceptionArgs m_args; public TExceptionArgs Args { get { return m_args; } } public Exception(String message = null, Exception innerException = null) : this(null, message, innerException) { } public Exception(TExceptionArgs args, String message = null, Exception innerException = null) : base(message, innerException) { m_args = args; } // The constructor is for deserialization; since the class is sealed, the constructor is // private. If this class were not sealed, this constructor should be protected [SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.SerializationFormatter)] private Exception(SerializationInfo info, StreamingContext context) : base(info, context) { m_args = (TExceptionArgs)info.GetValue(c_args, typeof(TExceptionArgs)); } // The method for serialization; it’s public because of the ISerializable interface [SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.SerializationFormatter)] public override void GetObjectData(SerializationInfo info, StreamingContext context) { info.AddValue(c_args, m_args); base.GetObjectData(info, context); } public override String Message { get { String baseMsg = base.Message; return (m_args == null) ? baseMsg : baseMsg + " (" + m_args.Message + ")"; } } public override Boolean Equals(Object obj) { Exception<TExceptionArgs> other = obj as Exception<TExceptionArgs>; if (obj == null) return false; return Object.Equals(m_args, other.m_args) && base.Equals(obj); } public override int GetHashCode() { return base.GetHashCode(); } } [Serializable] public abstract class ExceptionArgs { public virtual String Message { get { return String.Empty; } } } }
下面我使用这个泛型异常类来构造自己的异常类型:
首先定义一个计算两个城市距离的异常参数类,继承自ExceptionArgs。
[Serializable] public sealed class CalcDistanceErrorArgs : ExceptionArgs { string m_cityId1 = string.Empty; string m_cityId2 = string.Empty; public CalcDistanceErrorArgs(string cityId1, string cityId2) { this.m_cityId1 = cityId1; this.m_cityId2 = cityId2; } public override string Message { get { return string.Format("city1'id = '{0}', city2 'id = '{1}' ", m_cityId1, m_cityId2); } } }
然后编写一个函数,抛出一个这样的异常:
public static void ThrowMyOwnExceptionType() { throw new Exception<CalcDistanceErrorArgs>(new CalcDistanceErrorArgs("NanJing", "Beijing"), "获取不到这两个城市之间的距离"); }
使用下面的代码捕获异常:
try { ThrowMyOwnExceptionType(); } catch (Exception<CalcDistanceErrorArgs> e) { Console.WriteLine(e); }
运行时,我们查看异常如下图所示:
运行输出的异常信息如下:
ConsoleApplication1.Exception`1[ConsoleApplication1.CalcDistanceErrorArgs]: 获取不到这两个城市之间的距离 (city1'id = 'NanJing', city2 'id = 'Beijing' )
at ConsoleApplication1.SimpleExceptionTest.ThrowMyOwnExceptionType() in D:\Study Prj\ConsoleApplication1\ConsoleApplication1\SimpleExceptionTest.cs:line 57
at ConsoleApplication1.SimpleExceptionTest.Test() in D:\Study Prj\ConsoleApplication1\ConsoleApplication1\SimpleExceptionTest.cs:line 47
好了,希望这个泛型异常类对您自定义异常有所帮助吧。
异常和性能
下面的回复中有网友提出throw对性能会产生一定的影响,影响肯定是有的,我就不详细阐述了,请参考MSDN的两篇文章:
http://blogs.msdn.com/b/kcwalina/archive/2005/03/16/396787.aspx (Design Guidelines Update: Exception Throwing)(1.2 Exceptions and Performance)
http://msdn.microsoft.com/zh-cn/library/ms229009.aspx (异常和性能)