第7章 异常处理与程序调试
本章视频教学录像:27分钟
C#程序的编写和运行过程中,可能出现各种错误和意外情况,主要包括程序编写过程中的语法错误、逻辑错误,以及程序运行过程中的异常。C#提供了程序调试和异常处理机制,以保证程序的正确性和可靠性。本章介绍异常处理和程序调试的基本机制。
本章要点(已掌握的在方框中打钩)
□ C#错误类型
□ 异常处理概述
□ 异常处理过程
□ 程序调试
7.1 C#错误类型
本节视频教学录像:4分钟
编写C#程序的过程中不可避免地会出现各种各样的错误,如标点符号缺失、关键字写错,程序逻辑错误、运行时错误等。为了能够快速确定错误的原因,尽快排除程序错误,通常把程序错误划分为3种类型:语法错误、运行时错误和逻辑错误。通过程序调试可以发现并纠正这些错误。
1. 语法错误
语法错误是指代码不符合C#语言的语句。Visual Studio的编译系统能查出此类错误并报告错误的原因,有语法错误的代码不能通过编译,改正后才能通过编译。
如下图所示,在Visual Studio中运行包含语法错误的程序,则系统会提示“发生生成错误”,并在编辑窗口下方的错误列表中列出错误的数量且为每个错误给出具体说明。双击错误列表中某个错误行,系统自动将插入点跳转到代码窗口中该错误的对应位置。对于语法错误,根据错误提示信息修改,然后重新编译程序直到通过。
2. 运行时错误
运行时错误相对复杂一些,指的是在程序的运行过程中产生的错误,也就是异常。如数组下标越界、要打开的文件不存在等。
如下图所示,在Visual Studio中运行程序,如果产生运行时错误,则系统提示出现异常,并高亮显示引发异常的程序语句,方便程序设计者修改。
3. 逻辑错误
逻辑错误是指程序没有实现编程人员的设计意图和功能。有这类错误的程序可以运行,但是程序运行结果与预期不同。逻辑错误一般是因算法考虑不周引起的,也有些是因为编码时疏忽,如将“+”写成“*”等。
例如,下面的程序设计目标是求10的阶乘,由于保存结果的变量s的初值被赋值为0,导致最终输出结果为0,程序结果与设计目标不同,发生逻辑错误。
static void Main(string[] args)
{ int s=0; for (int i = 0; i <= 10; i++) s = s * i; Console.WriteLine(s); }
逻辑错误是Visual Studio无法识别的,要解决逻辑错误,则必须通过设置断点,进行程序调试,跟踪程序的运行过程,分析错误原因,然后修改程序代码。
7.2 异常处理概述
本节视频教学录像:4分钟
异常是程序执行时遇到的错误情况或意外行为,异常的出现通常是无法完全避免的。可能引发异常的原因有许多种,如在打开文件时文件不存在、网络资源不可用、读/写磁盘出错和内存申请失败等都可能引发异常,导致程序中止。程序员应该尽可能对可能出现的异常进行控制和处理,以保证程序运行过程的稳定可靠,避免程序的非正常终止。
7.2.1 异常和异常处理
异常处理就是编程人员在程序编写过程中对可能发生的错误和异常预先采取的处理措施。如可能存在异常,就要进行异常处理,以保证程序正常运行,适当的异常处理可以避免系统终止当前操作,否则程序可能会出现故障,系统也可能崩溃。例如,需要从键盘输入一个数字,当输入的是字符串时,则会由于输入数据的类型不正确导致程序无法正常继续运行。
static void Main(string[] args) { int x = int.Parse(Console.ReadLine()); … }
在以上代码中,程序试图从键盘输入数字存入变量x,但如果程序运行过程中,用户输入了一个字符串,如“hello”,则Visual Studio 2013会提示程序运行中遇到异常,并给出异常类型和相应说明。这里给出的是“格式异常”(FormatException),同时给出造成异常的原因:输入字符串的格式不正确。
7.2.2 异常类
异常通常是由应用程序(用户程序等)或运行库(公共语言运行库和应用程序运行库)引发的。.NET提供了大量与异常有关的类,用来处理异常,每一个异常类都表示一种异常。Exception类是所有异常的基类,当发生异常时,系统或当前正在执行的应用程序会通过引发包含关于该错误的信息的异常来报告异常。异常发生后,将由该应用程序或默认异常处理程序进行处理。
异常类继承关系的结构如下图所示。
有些异常在基本操作失败时由 .NET Framework的公共语言运行库(CLR)自动引发,如“零做除数时”就会引发DivideByZeroException异常。下表列出了常用的异常。
对于.NET类来说,一般的异常类System.Exception派生于System.Object,通常不在代码中抛出这个System.Exception对象,因为它无法确定错误的具体情况。通常用的异常类都从Exception类继承。其中有以下两种主要类型的异常类,它们构成了几乎所有的应用程序和运行库异常的基础。
⑴ ApplicationException:用户定义的应用程序异常类型的基类。ApplicationException继承Exception,但是不提供扩展功能,必须开发ApplicationException的派生类,以实现自定义异常的功能。
⑵ SystemException:系统异常类。CLR抛出的异常称为系统异常。这些异常通常被看作是不可恢复的、致命的错误。系统异常直接从名为System.SystemException的基类中派生,该基类又从System.Exception中派生。
System.Exception中有许多属性和方法,常用的如下表所示。
7. 3 异常处理过程
本节视频教学录像:9分钟
7.3.1 try-catch语句
try-catch语句是C#提供的异常处理语句,语法如下。
try { 可能出现异常的语句序列; } catch(异常类型 异常对象) { 对可能出现的异常进行处理; }
提示
try块中的语句是程序员希望程序实现的功能部分,但语句的执行过程中可能遇到异常。catch块中包含的代码处理各种异常,这些异常是try块中的语句执行时遇到的。如果try块中的代码正常执行,则catch块中的语句将不被执行。
【范例7-1】 异常处理例子,实现如字符串为空,抛出ArgumentNullException异常。
⑴ 启动Visual Studio 2013,新建一个控制台应用程序,项目名称为“TryCatchExam”。
⑵ 在Program.cs中的Main方法中添加如下代码。
01 try 02 {
03 int x=int.Parse(Console.ReadLine()); //输入整型值 04 int y=10; 05 int z=y/x; 06 } 07 catch(Exception e) //捕获异常,参数为异常类Exception的对象e 08 { 09 Console.WriteLine(e.Message); //输出被捕获的异常对象e的Message属性值 10 } 11 Console.ReadKey();
输入aaaa,程序执行结果如下图所示。
输入0,程序执行结果如下图所示。
【范例分析】
使用try-catch语句进行异常处理后,Visual Studio 2013将不再提示异常,异常处理由catch块的语句接管。捕获异常的流程是:程序运行进入try块,执行try块中的语句,当程序流离开try块后,如没有异常发生,将执行try-catch之后的语句。如果在try块中某个语句执行时遇到错误,程序流就跳转到catch块进行处理,执行catch块中的语句,进行异常处理。
例7-1中的异常是由CLR抛出的系统异常,第一次执行输入aaaa,try块执行到第3行出现异常,跳转到catch块,输出异常信息“输入字符串的格式不正确”。第二次执行输入0,try块执行到第5行出现异常,跳转到catch块,输出异常信息“尝试除以零”。本例在try块中遇到异常后由系统自动抛出异常,我们只负责异常处理。程序执行过程中可能出现不止一种异常,程序员需要尽可能考虑到所有情况并做好相应处理。
7.3.2 try-catch-finally语句
除了try-catch语句,C#异常处理语句还有try-catch-finally语句,语法如下。
try { 可能出现异常的语句序列; } catch(异常类型 异常对象) { 对可能出现的异常进行处理; }
finally { 最后要执行的代码,进行必要的清理操作,以释放资源 }
与try-catch语句相比,try-catch-finally语句多了一个finally块,无论try块的语句执行过程中是否发生异常,finally块中的语句都将得到执行,finally块的执行在try块和catch块之后。finally块可以包含执行清理的代码,例如,可以在finally块中关闭在try块中打开的连接或者打开的文件。
例如,在try块中打开文件,由于发生异常导致文件未被正常关闭,则需要在finally块中关闭文件。此外,在try-catch语句后边的语句也可以放到finally块内,例7-1可以改为:
01 try 02 { 03 //打开文件 04 int x=int.Parse(Console.ReadLine()); //输入整型值 05 int y=10; 06 int z=y/x; 07 //关闭文件 08 } 09 catch(Exception e) //捕获异常,参数为异常类Exception的对象e 10 { 11 Console.WriteLine(e.Message); //输出被捕获的异常对象e的Message属性值 12 } 13 finallly 14 { 15 //关闭文件 16 Console.ReadKey(); 17 }
7.3.3 throw语句
在try-catch和try-catch-finally语句中的try块中,除了由系统自动抛出异常外,也可以使用throw语句抛出异常;使用throw语句既可以引发系统异常,也可以引发自定义异常。throw使用格式如下。
throw异常对象;
例如,
throw new ArgumentNullException(); //抛出值不能为空的异常
throw new ArgumentNullException( ) 实例化了ArgumentNullException类的一个异常对象,并抛出。只要在try块语句的执行过程中遇到一个throw语句,就会立即转到与这个try块对应的catch块以进行异常处理。
【范例7-2】 异常处理例子,如果字符串为空,抛出ArgumentNullException异常。
⑴ 启动Visual Studio 2013,新建一个控制台应用程序,项目名称为“ThrowExam”。
⑵ 在Program.cs中添加一个方法ProcessString,代码如下。
01 static void ProcessString(string str) 02 { 03 if(str==null) //如果str参数为null,则抛出值不能为空的异常 04 { 05 throw new ArgumentNullException(); 06 } 07 }
⑶ 在Program.cs的Main方法中添加如下代码。
01 Console.WriteLine("输出结果为:"); //提示输出结果 02 try //try块用来放置可能有异常的代码 03 { 04 string str1=null; //声明一个字符串变量,赋值为空 05 ProcessString(str); //调用方法ProcessString() 06 } 07 catch(ArgumentNullException e) //捕获异常并输出异常信息 08 { 09 Console.WriteLine("这是异常:{0}",e.Message); 10 } 11 finally 12 { //finally块放置最后要执行的代码 13 Console.ReadKey(); 14 }
【范例分析】
比较例7-1和例7-2中catch块的参数类型,例7-1中catch块的参数e是System.Exception类的对象,Exception类是所有异常类的基类,因此例7-1中的catch块可以接收所有类型的异常,输出异常信息。例7-1中catch块的参数e是System. ArgumentNullException类的对象,catch块只能接收ArgumentNullException类的异常,如果try块中抛出的是其他异常则不接收。如果需要接收其他类型的异常,那么需要定义一个新的参数为该类型异常的catch块。由此,例7-1的try-catch语句可以改为如下。
try { … }
catch(FormatException e) { Console.WriteLine("这是FormatException,异常信息为:{0}", e.Message); } catch (DivideByZeroException e) { Console.WriteLine("这是DivideByZeroException,异常信息为:{0}", e.Message); }
在catch块中可以使用throw语句再次引发已由catch语句捕获的异常,这样做的意义是,可以将try-catch语句中处理不了的异常再次使用throw语句抛出到更高一级进行处理。
【范例7-3】 多次throw例子。
⑴ 启动Visual Studio 2013,新建一个控制台应用程序,项目名称为“ThrowExam2”。
⑵ 在Program.cs中添加方法FuncA和FuncB,代码如下。
01 static void FuncA() 02 { 03 throw new ArgumentException("This is exception in FuncA"); 04 } 05 static void FuncB() 06 { 07 try 08 { 09 FuncA(); 10 } 11 catch(Exception e) 12 { 13 throw; //再次抛出捕获的异常 14 } 15 }
⑶ 在Program.cs的Main方法中添加如下代码。
01 try 02 { 03 FuncB(); 04 } 05 catch(Exception e) 06 { 07 Console.WriteLine(e.Message); //输出异常消息 08 } 09 Console.ReadKey();
程序执行结果如下图所示。
【范例分析】
Main方法调用FuncB方法,FuncB方法调用FuncA方法。FuncA方法抛出异常,FuncB方法中的try-catch语句的catch块接收异常后再次抛出,Main方法接收FuncB中的catch块再次抛出的异常。这种情况通常发生在FuncB的try-catch语句接收到异常后,发现该异常无法处理或者该异常情况需要通知更高一级(即FuncB方法的调用者),再次将异常抛出给本方法的调用者,这里是Main方法去处理,当然,FuncB中的catch块也可以根据对异常的处理情况选择将收到的异常不加改变地再次抛出,或者修改异常的相关数据后再次抛出,甚至生成一个新的异常对象然后抛出,一切都取决于程序员设计程序的需要。此外,可以尝试一下,将FuncB中catch块的throw语句注释掉,看看Main方法是否还会接收到异常。
7.3.4 自定义异常类
如果系统提供的异常类不能够与程序中的异常相匹配,就需要自定义异常类。自定义异常类的语法如下。
class自定义异常类名:异常基类名 { //语句块 }
提示
一般从System.Exception类或其他常见异常类派生自定义异常类。异常类名称通常以Exception结尾,如NewException、MyException等。
一般要在自定义异常类中定义3个构造函数,一个是默认构造函数,一个用来设置消息属性,一个用来设置Message属性和InnerException属性,这三个构造函数可以从异常基类继承。自定义异常类时,也可以添加新的属性,但仅当新属性提供的数据有助于解决异常时,才将其添加到异常类。
【范例7-4】 自定义一个新的异常类TestException。
⑴ 在Program.cs中添加自定义异常类CustomException代码,代码如下。
01 public class TestException:Exception 02 { 03 public TestException():base() //继承基类的无参构造函数 04 { } 05 public TestException(string msg):base(msg) //继承基类有一个参数的构造函数 06 { }
07 public TestException(string msg,Exception inner):base(msg,inner) 08 { } //继承基类的有两个参数的构造函数 09 }
⑵ 在Program.cs的Main方法中添加以下代码。
01 Console.WriteLine("请输入整数"); //提示输入整数 02 try 03 { 04 int x=int.Parse(Console.ReadLine()); 05 if((x/2)*2==x) 06 throw new TestException("输入的是偶数!"); 07 else 08 throw new TestException("输入的是奇数!"); 09 } 10 catch(TestException e) 11 { 12 Console.WriteLine(e.Message); 13 } 14 Console.ReadKey();
程序执行结果如下图所示。
【范例分析】
自定义异常类比较简单,自定义异常类的使用方法与C#提供的异常类相同。本例在步骤⑴中定义了一个异常类,在步骤⑵中使用了try…catch结构来捕获自定义异常并处理。
7.4 程序调试
本节视频教学录像:7分钟
程序调试的主要目的是解决程序中的逻辑错误,通过设置断点,跟踪观察程序的执行过程,发现造成逻辑错误的具体语句,然后修改程序实现设计目标。
7.4.1 设置断点
程序的执行过程是连贯的,为了跟踪观察程序的运行状态,我们需要控制程序的运行过程,使得程序能够暂停在某些特定的位置,这种控制可以通过设置断点来实现。断点是程序暂停执行的地方,当程序运行到断点位置时,程序暂停执行,进入中断模式,程序设计者可以观察程序的运行状态,如某些变量的值,对程序进行分析。
在Visual Studio中打开需要调试的程序,在代码编辑窗口可以添加、删除断点。要在程序的某个语句所在位置添加或者删除断点,有以下几种方式。
单击代码窗口最左边灰色区域对应语句行的位置添加断点,再次单击删除断点,左侧的红色圆点即为断点。
单击语句所在行,然后按F9键添加断点,再次按【F9】键删除断点。
单击语句所在行,然后单击鼠标右键,在弹出的快捷菜单中选择【断点】【插入断点】命令。
7.4.2 启动、继续和停止调试
设置好断点以后,即可启动程序调试,要启动调试,可以使用以下方法。
● 选择菜单【调试】【启动调试】或者按【F5】键启动调试。
● 单击工具栏上的启动按钮 启动调试。
启动调试后,程序会暂停在第一个断点位置,观察完第一个断点位置的程序运行状态后,需要让程序继续调试,操作步骤与启动调试相同,只是菜单和工具栏上的文字都变成“继续”,这时程序继续执行并在下一个断点处暂停。
启动调试后,可以选择菜单【调试】【停止调试】或者单击工具栏上的停止调试按钮来停止程序调试。
7.4.3 单步调试
启动调试后,可以在菜单中选择【调试】【逐语句】或者【调试】【逐过程】进行逐语句或逐过程调试。
逐语句调试相当于为程序中的每个语句都加上一个断点,每次执行一条语句,也可以按【F11】键进行逐语句调试,遇到某一语句调用了其他函数,对该语句执行逐语句调试,则程序会暂停在该函数的第一行语句。
逐过程调试也可以通过按【F10】键实现,遇到某一语句调用了其他函数,对该语句执行逐过程调试,则程序会暂停在该语句的下一条语句,不再转入函数内部。
程序执行到被调用函数内部,需要停止逐语句调试时,可以选择【调试】【跳出】,程序将返回到调用函数。
7.4.4 调试监控
在程序调试过程中,通常需要监测程序的运行状态,Visual Studio提供了局部变量窗口、监视窗口、自动窗口和快速监视窗口等对程序的运行状态进行监控。
1. 局部变量窗口
选择菜单【调试】【窗口】【局部变量】,即可打开局部变量窗口。局部变量窗口允许查看在局部过程中声明的变量的当前值,但不能修改。
2. 自动窗口
选择菜单【调试】【窗口】【自动窗口】,即可打开自动窗口。自动窗口与局部变量窗口类似,但自动窗口显示当前语句和先前语句中使用的变量。此外,用户不能添加或删除自动窗口显示的项目,但可以修改某项的值。
3. 监视窗口
选择菜单【调试】【窗口】【监视】,有【监视1】、【监视2】、【监视3】和【监视4】4个监视窗口可用。可以从程序中选中变量或者表达式后拖动到监视窗口,用于判断程序的运行是否有错。
4. 快速监视窗口
选择菜单【调试】 快速监视】即可打开快速监视对话框。快速监视窗口为查看和计算变量与表达式提供了一个快捷的途径。其中,【重新计算】可以计算表达式的最新值,【添加监视】可以将表达式加入到监视窗口中。需要注意的是,必须关闭快速监视窗口才能继续进行程序调试。
7.5 高手点拨
本节视频教学录像:2分钟
7.5.1 使用多catch块处理异常
try-catch语句中的catch块可以捕获异常,被捕获的异常可以是所有异常或者程序员需要捕获的某类异常。实际上,一个try-catch语句中可以包含多个catch块,每个catch块用来捕获一类异常。出现异常时,将执行第一个能够处理该异常的catch块,而忽略其他catch块,即使其他catch指定的异常类型能够兼容要被处理的异常。
例如,
try { } catch(异常类型1)
{ } catch(异常类型2) { } … catch(异常类型n) { }
由于异常类型之间存在继承和派生关系,有可能某个异常能够被多个异常类型兼容。因此,catch块的排列次序需要从具体到通用,即把处理具体类型异常的catch块放在前边,而把兼容多种异常类型的catch块放在后边。
7.5.2 引发异常时要注意的问题
我们已经知道,异常机制的引入是为了处理程序执行时遇到的错误情况或意外行为。引发异常时需要注意以下问题。
(1)优先考虑使用System命名空间中提供的现有异常类型,除非特别需要,即某种错误或意外情况需要的处理方式与现有任何异常类型的处理方式都不相同,才需要自定义异常类型。
(2)尽可能引发最具体的异常类型。例如,如果某方法收到一个null参数,则该方法应引发System. ArgumentNullException,而不是引发该异常的基类型System.ArgumentException。也就是说,如果可能,尽可能引发子异常类型而非父异常类型,因为由父异常类型继承派生而来的子异常类型更加具体,更能准确描述错误和意外的情况。使用最具体的异常类型方便异常处理过程中对异常情况的判断和处理。
7.6 实战练习
一、思考题
1. 有几种类型的错误?
2. 如何实现自定义异常?
二、操作题
定义一个异常类DivZero,假如除数为零,则显示“除数不能为0”的异常信息。