第7章 异常处理
在C#编程语言中,异常的表现形式是Exception类的实例。当程序发生异常时,程序创建一个Exception类实例,放在异常堆栈中,等待应用程序的处理或者程序结束。在C#编程语言中,使用throw语句将异常抛出。当异常抛出时,程序在堆栈区寻找异常处理语句,如果未能找到处理程序,就抛出到当前所在的系统来处理。
7.1 创建自定义异常
在.NET Framework中,只要从Exception类继承,用户就能够定义自己的异常类,并定义该异常类的行为和数据,在程序中生成异常类的实例,通过throw向外部程序抛出异常信息。
技术要点
本示例主要说明了如何在程序中创建自定义的异常,技术要点如下。
● 自定义异常继承于Exception类,该类具有多种形式的构造函数,用户自定义的异常类只需要简单的继承这些构造函数即可。
● 为了使自定义类能够包含更多的信息,Exception类提供了Data属性,允许用户向Data中添加键值对(Key-Value)的数据。
● 在C#中使用throw语句抛出异常,如果异常未处理,将抛出到系统中,直到异常得到处理或者结束程序。
实现步骤
(1)创建控制台应用程序项目,命名为“CustomizeException”。
(2)打开并编辑Program.cs文件,代码如下所示。
using System; using System.Collections.Generic; using System.Text; namespace CustomizeException { class Program { static void Main(string[] args) { MyException myerr = new MyException();//创建一个异常类的实例 //获取异常类实例的数据并显示在屏幕上 Console.WriteLine("自定义异常的名称:{0}",myerr.Data["名称"].ToString()); Console.WriteLine("自定义异常的描述:{0}" , myerr.Data["描述"].ToString()); Console.WriteLine("自定义异常的关系:{0}" , myerr.Data["关系"].ToString()); Console.ReadLine(); throw myerr;//在程序中直接抛出异常,至此程序不能继续运行 } } class MyException : Exception//继承于Exception的自定义异常类 { public MyException()//默认的构造函数 { AddData();//在构造类实例的同时添加异常类数据 } public MyException(string message)//继承的构造函数,message表示异常的提示信息 : base(message) { AddData(); } //继承的构造函数,message表示异常的提示信息,inner表示包含的其他异常类实例 public MyException(string message, Exception inner) : base(message, inner) { AddData(); } private void AddData()//为异常类实例提供数据的方法 { this.Data.Add("名称", "自定义的异常");//添加键值为“名称”的字符串数据 this.Data.Add("描述", "用户根据业务规则定义的异常"); this.Data.Add("关系", "继承于Exception类"); } } }
(3)按F5键运行程序,运行结果如下所示。当按下回车键结束程序时,程序抛出异常,不能正常结束。
自定义异常的名称:自定义的异常 自定义异常的描述:用户根据业务规则定义的异常 自定义异常的关系:继承于Exception类
源程序解读
(1)本示例创建了一个继承于Exception的异常类MyException类。在该类的构造函数中,继承Exception构造函数的同时,还向Data属性添加键值数据。
(2)在本示例的主程序入口Main方法中,输出Data数据以后,抛出MyException类的实例myerr。此时程序未作任何异常处理,程序不能正常结束。
7.2 获取异常信息
Exception类具有多种属性,根据这些属性能够获取关于异常的信息。了解这些信息,有助于查清引起异常的原因,定位异常发生的位置,为处理异常提供了充分的信息。
技术要点
本示例主要说明了如何在程序中获取异常的信息,技术要点如下。
● 使用Exception类提供的属性,可以获取描述异常的信息。这些信息包含了引起异常的来源信息和关于异常的文字描述。
● 使用Exception类的StackTrace属性能够了解异常的调用堆栈,该属性按照堆栈中自底向上的调用次序,列出当前表示的异常在哪些调用中抛出。
实现步骤
(1)创建控制台应用程序项目,命名为“ExceptionInfo”。
(2)打开并编辑Program.cs文件,代码如下所示。
using System; using System.Collections.Generic; using System.Text; namespace ExceptionInfo { class Program { static void Main(string[] args) { try { //试图将字符串赋值给整型变量时,将会引起异常,从而能输出异常信息 int.Parse("Error"); } catch (Exception err) { //获取并输出描述异常的文字信息 Console.WriteLine("当前异常的描述为:"+err.Message); //获取并输出引发异常的方法名称 Console.WriteLine("引起异常的方法是:"+err.TargetSite.Name); //获取并输出异常的来源名称,通常是应用程序的名称 Console.WriteLine("异常的来源名称是:"+err.Source); //获取并输出异常产生时,调用堆栈上的程序位置 Console.WriteLine("异常在堆栈中的帧:"+err.StackTrace); Console.ReadLine(); } } } }
(3)按F5键运行程序,运行结果如下所示。
当前异常的描述为:输入字符串的格式不正确。 引起异常的方法是:StringToNumber 异常的来源名称是:mscorlib 异常在堆栈中的帧: 在 System.Number.StringToNumber(String str, NumberStyles op tions, NumberBuffer& number, NumberFormatInfo info, Boolean parseDecimal) 在 System.Number.ParseInt32(String s, NumberStyles style, NumberFormatInfo in fo) 在 System.Int32.Parse(String s) 在 ExceptionInfo.Program.Main(String[] args) 位置 C:\Program\200\ExceptionInfo\ExceptionInfo\Program.cs:行号 13
源程序解读
(1)本示例在程序中故意引起一个异常,从该异常中分别获取异常的描述文字、方法、来源名称和堆栈中的帧。
(2)在一般的应用中,只需要查看Message属性即可了解引起异常的原因,如果是自定义异常,Message属性未描述清楚的情况,就有必要使用其他的属性,了解是何处抛出的异常,并且能够通过调用堆栈信息,明确调用的次序和位置,更好地处理异常。
7.3 使用finally关键字
在C#编程语言中,finally关键字出现在try语句中包含在finally之后“{}”符号对中的代码段,不管try语句中的执行结果如何,总是得到执行。因为finally关键字的这个特点,所以经常在finally语句中包含资源的回收功能,例如关闭打开的数据库连接,关闭打开的文件等。
技术要点
本示例主要说明了如何在程序中使用finally关键字,技术要点如下。
● finally关键字必须在try语句中使用,而不能用于其他场合。
● 不管try语句中的执行结果如何,finally关键字之后“{}”符号对所包含的代码总是得到执行。
● 在抛出异常而未处理时,程序不能正常运行。
实现步骤
(1)创建控制台应用程序项目,命名为“FinallyExample”。
(2)打开并编辑Program.cs文件,代码如下所示。
using System; using System.Collections.Generic; using System.Text; namespace FinallyExample { class Program { static void Main(string[] args) { int i = 6; try { MyException myerr = new MyException("自定义异常");//创建自定义异常的实例 throw myerr;//此句将产生异常 } finally//不管try语句的执行结果,以下“{}”符号对中所包含的代码总是执行 { Console.WriteLine("变量i的值为:{0}",i); } } } class MyException : Exception//自定义的异常类 { public MyException(string message) : base(message)//继承Exception类的构造函数 { } public MyException(string message, Exception inner) : base(message, inner)//继承Exception类的构造函数 { } } }
(3)按Ctrl+F5组合键运行程序,在弹出的异常信息窗口中,点击“调试”按钮,在Visual Studio实时调试器窗口中,单击“否”按钮,运行结果如下所示。
未处理的异常: FinallyExample.MyException: 自定义异常 在 FinallyExample.Program.Main(String[] args) 位置 C:\Program\200\FinallyExam ple\FinallyExample\Program.cs:行号 14 变量i的值为:6 请按任意键继续 . . .
源程序解读
(1)本示例自定义了一个继承于Exception类的MyException类,在该类中继承Exception的两个构造函数。
(2)在主程序入口Main方法中,定义一个整型变量i,然后在try语句后的代码段中创建自定义异常MyException类的实例myerr,并抛出myerr异常。代码执行到此处,因为异常未得到处理,所以不能继续正常执行。只有在不调试执行的情况下带有异常信息地执行,才能看到finally中的代码段实际上已经运行。
7.4 使用try-catch语句捕获异常
try-catch语句是C#编程语言中最为常见的异常处理语句。在该语句中,一个包括“{}”中的代码段的try语句之后,允许带有多个catch语句,用于捕获多种不同的异常。
技术要点
本示例主要说明了如何在程序中使用try-catch语句捕获异常,并获取异常中所包含的信息,技术要点如下。
● 在try-catch语句中需要使用的变量,应该在try语句之前声明。
● 在程序中应该允许特定的异常向上堆栈传递,这样做是为了使异常信息能够完整地传递到调用代码,不建议在程序中排除、隐藏异常,不带参数的catch既不产生Exception类实例,也不产生继承于Exception类的实例,将排除程序产生的异常信息。当catch语句中的参数为Exception类实例时,将会隐藏特定的异常信息。这两种方法只能用于比较特殊的场合,一般的情况下不建议使用。
● 当捕捉并再次引发异常时,例如,在传递异常的时候,最好的方式是使用throw的空引发。空引发不会改变堆栈中引发异常的位置,如果不使用throw空引发,就会将堆栈中引发异常的位置指向当前的throw语句。
实现步骤
(1)创建控制台应用程序项目,命名为“Try-Catch”。
(2)打开并编辑Program.cs文件,代码如下所示。
using System; using System.Collections.Generic; using System.Text; namespace Try_Catch { class Program { static void Main(string[] args) { //在try语句的外部声明变量,否则catch语句所包含的代码段无法识别i变量 int i = 0; try { i = int.Parse("Error");//此处代码在将字符串转换为整型时引起异常 } catch//catch语句之后不带任何参数,表示处理任何异常 { Console.WriteLine("不作异常捕捉的处理,变量i的值为:{0}",i); } try { ThrowException(); } //在未明错误的情况下使用所有异常的基类来获取异常信息,不建议使用 catch (Exception currerr) { Console.WriteLine("引起异常的方法是:{0}",currerr.TargetSite.Name); } Console.ReadLine(); } static void ThrowException()//抛出异常的方法 { try { //创建一个自定义异常的实例 MyException err = new MyException("自定义异常的信息"); throw err;//抛出异常 } catch (MyException e)//捕获指定类型的异常 { Console.WriteLine(e.Message); //ThrowException方法的目的是为了传输异常,在此处抛出使调用程序处理 throw; } } } class MyException : Exception//自定义的异常类,继承于Exception类 { public MyException(string message) : base(message)//继承Exception类的构造函数 { } } }
(3)按F5键运行程序,运行结果如下所示。
不作异常捕捉的处理,变量i的值为:0 自定义异常的信息 引起异常的方法是:ThrowException
源程序解读
(1)本示例创建了一个自定义的异常类MyException,并在Program类中定义了一个静态的ThrowException方法,在该方法中创建MyException类的实例err,通过捕捉异常将err表示的异常信息传递给主程序入口Main方法的调用语句。本示例的程序流程图如图7.1所示。
图7.1 try-catch示例程序流程图
(2)在try语句开始之前,应该首先声明在整个try-catch结构中需要使用的变量。在本示例程序中,i变量必须在try语句之前声明,否则catch语句所包含的代码段将无法识别i变量。
(3)在未明错误的情况下,可以使用Exception作为catch语句的参数类型,在这种情况下,程序无法判断是异常的数据类型,即捕捉的是非特定的异常,此时可能会忽略错误代码的处理,在实际编程中不建议使用。
(4)在ThrowException方法中,因为该方法的目的是抛出异常让主程序入口Main方法来处理,所以在该方法中的异常,不应该进行处理,而是通过throw语句,将该异常向上抛出到程序堆栈中。此时调用语句产生异常,并在后续的catch语句中处理。
(5)在ThrowException中,catch块的throw语句是空引发,此时抛出的异常,在堆栈中指向的位置是try语句中的throw语句,如果使用了非空引发,例如,在catch中抛出异常时使用了如下代码:
throw e;
以上代码抛出的异常,在堆栈中指向的位置,是位于catch块中的语句。这种非空引发将改变异常抛出的位置,不利于程序的调试和异常处理,不建议使用。
7.5 使用多catch语句捕获异常
在C#编程语言中允许try语句存在多个catch语句异常捕捉,在多个catch语句并存的情况下,捕捉异常的次序和处理十分关键。在C#程序中,将首先捕获特定程度较高的异常,如果将特定程度较低的异常捕捉放在前面,就会在编译时发生错误。
技术要点
本示例主要说明了在程序中使用多catch语句捕获异常的方法,技术要点如下。
● 在使用多catch语句捕捉异常的时候,应把特定程度最高的捕捉放在最前面。
● catch语句可以不使用任何参数,不存在Exception类及其派生类的实例,此时如果要抛出异常,可以使用不带参数的空引发throw。
● 使用Exception类型作为参数的catch语句和不使用任何参数的catch语句不能在同一个try-catch结构中并存。
● 在try-catch-finally结构的代码中,建议的做法是:将变量的定义和初始化放在try语句之前,将可能出现异常的代码放在try代码段中,将各种可能产生的异常分别捕捉,并放置在各自的catch代码段中处理。在finally中的代码段主要作用是清理占用的资源,例如关闭数据库连接、释放文件或内存的占用等。
实现步骤
(1)创建控制台应用程序项目,命名为“MultiCatch”。
(2)打开并编辑Program.cs文件,代码如下所示。
using System; using System.Collections.Generic; using System.Text; namespace MultiCatch { class Program { static void Main(string[] args) { MyException1 myerr1 = new MyException1();//创建MyException1异常类的实例 MyException2 myerr2 = new MyException2();//创建MyException2异常类的实例 MultiCatch(myerr1); MultiCatch(myerr2); MultiCatch(new Exception());//使用未知异常 Console.ReadLine(); } static void MultiCatch(Exception e) { try { throw e;//抛出传入的异常以使用catch判断 } catch (MyException1 myerr)//对于MyException1异常的处理 { Console.Write("当前异常:"+myerr.Message); } catch (MyException2 myerr)//对于MyException2异常的处理 { Console.Write("当前异常:"+myerr.Message); } catch//对未知异常的处理 { Console.Write("未知异常"); } finally { Console.WriteLine(""); } } } class MyException1 : Exception//自定义的异常类 { public MyException1() : base("自定义异常类1") { } public MyException1(string message) : base(message) { } } class MyException2 : Exception//自定义的异常类 { public MyException2() : base("自定义异常类2") { } } }
(3)按F5键运行程序,运行结果如下所示。
当前异常:自定义异常类1 当前异常:自定义异常类2 未知异常
源程序解读
(1)本示例定义了两个自定义的异常类,即MyException1类和MyException2类,这两个类的实例作为参数传递给MultiCatch方法,在MultiCatch方法中抛出异常,然后分别捕捉可能产生的异常,在处理异常时输出异常的描述信息。程序还创建一个Exception类的实例,作为不带参数的catch的输出。本示例程序的流程如图7.2所示。
图7.2 使用多catch实例程序流程图
(2)在MultiCatch方法中,捕获异常的顺序是MyException1、MyException2、不带参数的异常捕获。在捕获的过程中,首先捕获的是特定程度较高的异常。在参数e是MyException1异常或者MyException2异常时,不会执行无参数catch中的代码段。
(3)在本示例中的自定义异常类中,使用了继承带参数的构造函数来实现不带参数的构造函数。因为在构造函数中已经指定了Message属性,所以在创建MyException1类实例和MyException2类实例的时候,无须再指定异常的Message属性。