异常处理的作用
无论编码技术有多好,程序都必须处理任何可能出现的错误。如果应用程序不对异常进行任何处理,直接抛出最终会导致应用程序退出。为了提高程序的健壮性,必须对任何异常进行必要的处理。
异常执行流程
异常处理的关键:
1、 定义什么错误
2、 判断错误
3、 从错误中恢复
如果在try块中抛出一个异常,CLR将搜索捕捉类型与异常类型相同的catch块,如果没有找到,CLR会调用栈的更高一层搜索与异常类型匹配的捕捉类型,如果到了栈的顶部还未找到,就会发生一个未处理异常。一旦找到一个匹配的catch块,所有内层的finally块会被执行;如果没有找到,内层的finally块是不会执行的,这一点要特别注意。接着执行匹配catch块内容,最后是该catch块对应的finally块(如果有的话)。
下面代码展现了异常处理的关键流程。
private void SomeMethod() { try { //可能会抛异常的代码放在这里 } catch (InvalidOperationException) { //从InvalidOperationException恢复的代码放在这里 } catch (IOException) { //从IOException恢复的代码放在这里 } catch { //从除上面的异常外的其他异常恢复的代码放在这里 //… //捕捉到任何异常,通常要重新抛出异常。 throw; } finally { //这里的代码总是执行,对始于try块的任何操作进行清理 //这里的代码总是执行 } // 如果try块没有抛出异常,或异常被捕获后没有抛出或没有重新抛出异
//常,就执行这里的代码。 }
下面代码展现三种抛出异常方式对性能的影响
internal class Program { private static void Main(string[] args) { int times = 3000; string test = "test"; int a = 0; Stopwatch stopwatch; //TryParse模式 stopwatch = Stopwatch.StartNew(); for (int i = 0; i < times; i++) { Int32.TryParse(test, out a); } stopwatch.Stop(); Console.WriteLine("TryParse模式避免异常:" + stopwatch.ElapsedMilliseconds); //抛出指定的异常实例 stopwatch.Reset(); stopwatch.Start(); for (int i = 0; i < times; i++) { try { if (!int.TryParse(test, out a)) { throw new ArgumentException(test); } } catch { } } stopwatch.Stop(); Console.WriteLine("抛出指定的异常:" + stopwatch.ElapsedMilliseconds); //在嵌套层里面不做任何异常处理直接往顶层抛出 stopwatch.Reset(); stopwatch.Start(); for (int i = 0; i < times; i++) { try { MyClass1.Method1(); } catch { } } stopwatch.Stop(); Console.WriteLine("在嵌套层里面不做任何异常处理直接往顶层抛出:" + stopwatch.ElapsedMilliseconds); // 嵌套层已处理异常,得体恢复返回 stopwatch.Reset(); stopwatch.Start(); for (int i = 0; i < times; i++) { MyClass4.Method4(); } stopwatch.Stop(); Console.WriteLine("嵌套层已处理异常,得体恢复返回:" + stopwatch.ElapsedMilliseconds); Console.ReadLine(); } } internal class MyClass1 { public static void Method1() { MyClass2.Method2(); } } internal class MyClass2 { public static void Method2() { MyClass3.Method3(); } } internal class MyClass3 { public static void Method3() { int.Parse("test"); } } internal class MyClass4 { public static void Method4() { MyClass5.Method5(); } } internal class MyClass5 { public static void Method5() { MyClass6.Method6(); } } internal class MyClass6 { public static string Method6() { string a = ""; try { int.Parse("test"); } catch { a = ""; } return a; } }
运行结果:
从运行结果来看指定抛出异常性能比直接抛异常好一点,为什么会这样?请拍砖。在嵌套层里面进行异常处理与直接往顶层抛异常的性能相差不大,
但最重要的地方是对错误进行恢复。异常处理的重点是错误的恢复、处理。
微软对异常处理方式建议
对于可能在常见方案中引发异常的成员,可以考虑使用 Tester-Doer 模式来避免与异常相关的性能问题。
例如,对于函数的参数问题,就必须创建检测类检测输入是否正确。对于可能在常见方案中引发异常的成员,可以考虑使用 TryParse 模式来避免与异常相关的性能问题。
TryParse模式参考int.TryParse等.就是创建正常运行的条件避免抛出异常.
对于可能在常见方案中引发异常的成员,可以考虑使用 Tester-Doer 模式来避免与异常相关的性能问题。
Tester-doer 模式将分为了调用,可能会引发异常,分成两个部分: 一种测试仪和实干家。 Tester 对可能导致 Doer 引发异常的状态执行测试。 测试恰好插入在引发异常的代码之前,从而防范异常发生。
下面的代码示例演示此模式的 Doer 部分。 该示例包含一个方法,在向该方法传递 null(在 Visual Basic 中为Nothing)值时该方法将引发异常。 如果频繁地调用该方法,就可能会对性能造成不良影响。
public class Doer { // Method that can potential throw exceptions often. public static void ProcessMessage(string message) { if (message == null) { throw new ArgumentNullException("message"); } } // Other methods... }
下面的代码示例演示此模式的 Tester 部分。 该方法利用一个测试来避免在 Doer 将引发异常时调用 Doer (ProcessMessage)。
public class Tester { public static void TesterDoer(ICollection<string> messages) { foreach (string message in messages) { // Test to ensure that the call // won't cause the exception. if (message != null) { Doer.ProcessMessage(message); } } } }
对于可能在常见方案中引发异常的成员,可以考虑使用 TryParse 模式来避免与异常相关的性能问题。若要实现 TryParse 模式,
需要为执行可在常见方案中引发异常的操作提供两种不同的方法。 第一种方法 X, 执行该操作并在适当时引发异常。
第二种方法 TryX, 不引发异常,而是返回一个 Boolean 值以指示成功还是失败。
由对 TryX 的成功调用所返回的任何数据都通过使用 out(在 Visual Basic 中为 ByRef)参数予以返回。Parse 和 TryParse 方法就是此模式的示例。
为每个使用 TryParse 模式的成员提供一个引发异常的成员。只提供 TryX 方法几乎在任何时候都不是正确的设计,因为使用该方法需要了解 out 参数。
此外,对于大多数常见方案来说,
异常对性能的影响不会构成问题;因此应在大多数常见方案中提供易于使用的方法。
结论: 由微软提供的Doer-Tester模式可知处理异常的方式最好是不要引发异常,在可能抛出异常的地方进行必要的检查和验证。例如对实参进行验证。
总结:实现自己的方法时,如果方法无法完成方法名所指明的任务,就应该抛出异常或对错误进行得体恢复。抛出异常时应该详细说明方法为什么无法完成任务。对于未指定处理的异常通常被写入日志,开发人员必须修复该异常所在的bug。