1 错误与异常
程序中的错误有很多种,最典型的一种是语法错误,另一种是代码中的逻辑错误,代码本身没有语法错误,可能在运行过程中出现。
2 C#中的异常处理结构
C#语言通过try语句提供的控制结构来检测代码中的异常并作出相应的处理。try语句有4种使用方式。
2.1 try-catch语句
正常情况下try代码段中语句依次执行,而catch代码段不会被执行;一是出现异常,程序控制权就从try语句转到catch语句,并在catch代码段中进行异常处理。
catch语句可以捕获指定的异常。可以在catch语句上加一对括号,在括号中指定希望捕获的异常类型。
class ExtractExceptionSample { static void Main() { try { Console.Write("请输入整数x:"); int x = int.Parse(Console.ReadLine()); Console.Write("请输入整数y:"); int y = int.Parse(Console.ReadLine()); int result = 24 / (6 - x) / (x - y) / (y - 2); Console.WriteLine("24 / (6 - x) / (x - y) / (y -2) = {0}", result); } catch (FormatException) { Console.WriteLine("输入的格式不正确"); } catch (DivideByZeroException) { Console.WriteLine("分母不能为零"); } catch (Exception) { Console.WriteLine("其它错误"); } } }
同时使用多个catch语句时,如果其中两个cathc语句所捕获的异常类存在继承关系,那么要保证捕获派生类的catch语句在前,而捕获基类的catch语句在后。上例中,Exception是所有异常类的基类。
2.2 try-catch-finally语句
其中同样可以使用多个catch语句,但finally语句要在所有catch语句的后面,而且只能出现一次。不论程序在执行过程中是否发生异常,finally语句中的代码段总是被执行。
2.3 try-finally语句
由于没有catch语句,实际不并不能处理异常。如果发生异常,该异常将在执行完finally代码段之后被抛出。
2.4 throw语句
前面介绍的3种语句是用于防止异常出现时程序中止,而throw语句相反,它主动引发一个异常,如果该异常不被捕获将导致程序中止。throw语句的使用格式是在关键字throw之后跟一个异常类型的对象,当程序执行到throw语句时就引发相应的异常,之后的语句不会被执行。throw语句的主要用途是对发生的异常进行描述。
class ThrowSample { static void Main() { ConsoleRW c1 = new ConsoleRW(); c1.Validate("2004", 5); } } public class ConsoleRW { public string Read(string sPrompt) { Console.WriteLine("请输入{0}:", sPrompt); return Console.ReadLine(); } public void Write(string sPrompt, string sContent) { Console.WriteLine("{0}: {1} ", sPrompt, sContent); } public void Validate(string sPwd, int iCount) { int i = 0; while (Read("密码") != sPwd) { Console.WriteLine("密码错误!"); i++; if (i > iCount) throw (new Exception("密码错误次数超过限制,您没有系统的访问权限。")); } Console.WriteLine("通过验证"); } }
如果发生多次输入错误,程序的输出将如下:
请输入密码:20045密码错误!请输入密码:251密码错误!请输入密码:455密码错误!请输入密码:126密码错误!请输入密码:12密码错误!请输入密码:154密码错误!未经处理的异常: System.Exception: 密码错误次数超过限制,您没有系统的访问权限。 在 P16_7.ConsoleRW.Validate(String sPwd, Int32 iCount) 位置 F:\编程学习\C#2.0\csharp2\P16_7\ConsoleRW.cs:行号 29 在 P16_7.ThrowSample.Main() 位置 F:\编程学习\C#2.0\csharp2\P16_7\ThrowSample.cs:行号 13请按任意键继续. . .
如果在try-catch语句或try-catch-finally语句的try代码段中使用了throw语句,而throw语句产生的异常又被之后的catch语句捕获,那么就在该catch代码段中进行相应的异常处理。而在其它情况下,throw语句产生的异常都会导致代码中止,这也包括在catch代码段中使用的throw语句。
3 异常的层次结构
3.1 异常传播
当异常在try代码段中被引发时,程序控制权将在异常处理结构中转移,直到找到一个能够处理该异常的catch语句,否则中止程序,这个过程叫做异常传播。异常传播的步骤为:
(1)如果当前的异常处理结构中上包含能够处理该异常的catch语句,那么程序控制权就转移给第一个这样的catch语句,异常传播结束。
(2)如果没有找到能够处理该异常的catch语句,则程序通过当前的异常处理结构(如果存在finally代码段则执行它)。
(3)如果程序到达更外层的一个异常处理结构,则转到第(1)步;
(4)如果异常在当前的成员方法中没有得到处理,则当前方法的执行代码被中止;若当前方法是程序所在的进程或线程的主方法,则整个程序结束运行;
(5)程序控制权转移给调用当前方法的代码,重复第(1)步。
class ExceptionPropagateSample { static void Main() { try { OutterMethod(0); OutterMethod(1); OutterMethod(2); } catch (Exception) { Console.WriteLine("发生一般异常"); } } public static void OutterMethod(int x) { if (x == 2) throw new Exception(); try { MiddleMethod(x); } catch (ArithmeticException) { Console.WriteLine("发生算术异常"); } } public static void MiddleMethod(int x) { if (x == 1) throw new ArithmeticException(); try { InnerMethod(x); } catch (DivideByZeroException) { Console.WriteLine("发生除法异常"); } } public static void InnerMethod(int x) { if (x == 0) throw new DivideByZeroException(); } }
输出结果:
发生除法异常发生算术异常发生一般异常请按任意键继续. . .
3.2 Exception类
Exception类是.NET类库中所有其它异常类的基类,即是对所有异常的一般抽象。其构造函数可以不带参数,也可以指定一个字符类型的参数作为描述异常的信息。还可以指定另一个异常对象作为参数构造Exception对象,这表示作为参数的异常对象引发了正在构造的异常对象。
(参见Exception类的公用属性)。
Exception类还提供了一个公有方法GetBaseException,用于返回异常的根源。因为一个异常可能引发另一个异常,使用该方法可以到异常链中第一个异常对象。如果异常链中只有当前异常对象,那么调用该方法得到的总是对象本身。
在catch语句中,除了可以指明要捕获的异常,还可以声明捕获到的异常对象。通过这个对象的属性或方法,就可以得到关于当前异常对象的详细描述。看下面的程序:
class ExceptionDetailSample { static void Main() { try { OutterMethod(0); } catch (Exception exp) { Console.WriteLine("程序运行过程中发生异常。"); Console.WriteLine("\n错误信息:\n" + exp.Message); Console.WriteLine("\n引发对象:\n" + exp.Source); Console.WriteLine("\n帮助文件:\n" + exp.HelpLink); Console.WriteLine("\n内部异常:\n" + exp.InnerException); Console.WriteLine("\n堆栈表示:\n" + exp.StackTrace); Console.WriteLine("\n引发方法:\n" + exp.TargetSite.Name); Console.WriteLine("\n基础异常:\n" + exp.GetBaseException()); } } public static void OutterMethod(int x) { if (x == 1) throw new Exception("发生一般异常:x不能为1"); InnerMethod(x); } public static void InnerMethod(int x) { if (x == 0) throw new ArithmeticException("发生算术异常:x不能为0"); } }
程序运行过程中发生异常。错误信息:发生算术异常:x不能为0引发对象:P16_9帮助文件:内部异常:堆栈表示: 在 P16_9.ExceptionDetailSample.InnerMethod(Int32 x) 位置 F:\编程学习\C#2.0\csharp2\P16_9\ExceptionDetailSample.cs:行号 39 在 P16_9.ExceptionDetailSample.OutterMethod(Int32 x) 位置 F:\编程学习\C#2.0\csharp2\P16_9\ExceptionDetailSample.cs:行号 33 在 P16_9.ExceptionDetailSample.Main() 位置 F:\编程学习\C#2.0\csharp2\P16_9\ExceptionDetailSample.cs:行号 14引发方法:InnerMethod基础异常:System.ArithmeticException: 发生算术异常:x不能为0 在 P16_9.ExceptionDetailSample.InnerMethod(Int32 x) 位置 F:\编程学习\C#2.0\csharp2\P16_9\ExceptionDetailSample.cs:行号 39 在 P16_9.ExceptionDetailSample.OutterMethod(Int32 x) 位置 F:\编程学习\C#2.0\csharp2\P16_9\ExceptionDetailSample.cs:行号 33 在 P16_9.ExceptionDetailSample.Main() 位置 F:\编程学习\C#2.0\csharp2\P16_9\ExceptionDetailSample.cs:行号 14请按任意键继续. . .
3.3 其它一些常见的异常类
3.3.1 SystemException类和ApplicationException类
它们是Exception的直接派生类中最常用的两个。SystemException类是System命名空间中所有其它异常类的基类,ApplicationException类则表示应用程序发生非致命性错误所引发的异常。和Exception类相比,这两个类并没有提供新的属性或方法。由公共语言运行时引发的异常,通常使用SystemException;而由应用程序自身引发的异常,则通常使用ApplicationException。这只是.NET框架中处理异常的一个建议性的规则,并没有强制性。
3.3.2 与参数有关的异常类
ArgumentException类和FormatException类都表示由于传递给方法成员的参数发生错误引发的异常,它们都是SystemException的直接派生类。FormatException类在前面已经出现过,表示参数格式错误;而ArgumentException类则表示参数无效。除了继承的属性之外,ArgumentException类还提供了一个string类型的属性ParamName,表示引发异常的参数名称。
ArgumentException还有两个常用的派生类,其中ArgumentNullException表示将空值null作为参数传递给了方法,而ArgumentOutOfRangeException则表示传递给方法的参数值超出了可接受的范围。
下面的程序由用户输入年、月、日来构造一个DateTime对象。如果输入的年月日不是整数,则捕获FormatException异常;如果输入超出可接受的范围,则捕获ArgumentOutOfRangeException异常:
class ArgumentExceptionSample { static void Main() { try { Console.Write("请输入年份:"); int year = int.Parse(Console.ReadLine()); Console.Write("请输入月份:"); int month = int.Parse(Console.ReadLine()); Console.Write("请输入日期:"); int day = int.Parse(Console.ReadLine()); DateTime dt1 = new DateTime(year, month, day); Console.WriteLine("输入时间为:" + dt1.ToLongDateString()); } catch (FormatException) { Console.WriteLine("输入错误:年月日应当是整数"); } catch (ArgumentOutOfRangeException) { Console.WriteLine("输入错误:不是有效的时间格式"); } } }
3.3.3 与成员访问有关的异常类
MemberAccessException类表示访问类的成员失败所引发的异常。失败的原因可能是没有足够的访问权限,也可能是要访问的成员根本不存在。例如,在调用代表类Delegate的DynamicInvoke方法时,如果无法访问代表所封装的方法,就会引发MemberAccessException异常。
MemberAccessException类的直接派生类有:
• FieldAccessException,表示访问字段成员失败所引发的异常;
• MethodAccessException,表示访问方法成员失败所引发的异常;
• MissingMemberException,表示访问的成员不存在时所引发的异常。
3.3.4 与数组有关的异常
当访问的下标超过了数组的长度时,将引发IndexOutOfRangeException异常;如果试图在数组中存储不正确的元素,将引发ArrayTypeMismatchException异常;而如果使用了维数错误的数组,将引发RankException异常。这3个类也都是SystemException的直接派生类。
3.3.5 与内存和磁盘操作有关的异常
涉及到内存和磁盘操作时,引发异常的原因就复杂了:既可以是软件本身的错误,也可能是系统硬件的问题。
如果程序的运行得不到足够的内存,将引发OutOfMemoryException异常。如果程序引用了内存中的空对象,将引发NullReferenceException异常,该异常类需要和前面介绍的ArgumentNullException类相区别。例如一个对象为空值null,试图调用该对象的字段或方法成员就会引发NullReferenceException异常;而如果将该对象作为一个参数传递给某个方法,而该方法不支持空对象,那么引发的是ArgumentNullException异常。
IOException类表示在进行文件输入输出操作时所引发的异常,它的5个直接派生类分别是:
• DirectoryNotFoundException,表示没有找到指定的目录而引发的异常;
• FileNotFoundException,表示没有找到指定的文件而引发的异常;
• EndOfStreamException,表示已经到达流的末尾而引发的异常;
• FileLoadException,表示不能加载文件而引发的异常;
• PathToolLongException,表示文件或目录的路径名超出规定的长度而引发的异常。
3.3.6 与算术运算有关的异常
ArithmeticException类表示与算术运算有达的所有异常类的基类。其派生类有:
• DivideByZeroException,表示整数或十进制运算中试图除以零时所引发的异常;
• NotFiniteNumberException,表示浮点数运算中出现正负无穷大或非数值时所引发的异常;
• OverflowException,表示运算溢出所引发的异常。