总体要求

  1、了解程序错误的三种类型。

  2、熟练运用VS2019的调试器调试程序错误。

  3、了解异常和异常处理的概念。

  4、学会使用try...catch..finally及throw语句来捕获和处理异常。

相关知识点

  1、程序错误的分类。

  2、VS 2019调试器的调试方式。

  3、异常处理。

学习重点

  1、调试程序错误的方法。

  2、try...catch...finally结构及其使用方法。

学习难点

  1、多重catch块对不同异常的捕获

  2、自定义异常。

7.1 程序错误

  在软件开发过程中,程序出现错误是十分常见的,不论多么有经验的程序员,也无法保证程序没有任何错误。因此排除程序的错误是必不可少的工作。VS2019提供了完善的程序错误调试功能,可以快速地发现和定位程序中地错误,并进行修正。

  7.1.1 程序错误分类

  在编写程序时,经常会遇到各种各样地错误,这些错误中有些容易发现和解决,有些则比较隐蔽甚至很难发现。C#程序错误总体上可以归纳为三类:语法错误、逻辑错误和运行时错误。

  1.语法错误

  语法错误是指不符合C#语法规则的程序错误。例如,变量名的拼写错误、数据类型错误、标点符号的丢失、括号不匹配等。语法错误是三类程序中最容易发现也是最容易解决的一类错误,发生在源代码的编写过程中。在VS 2019中,源代码编辑器能自动识别语法错误,并用红色波浪线标记错误。只要将鼠标停留在带有此标记的代码上,就会显示出其错误信息,同时显示在错误列表窗口中。如图7-1所示,有两处错误,第一个波浪线表示不能将一个string类型的值(txtNum.Text)直接赋值给一个int类型的变量(x)。第二个波浪线表示语句应该以英文分号结尾,而不是以中文分号结尾。

 

 

 图 7-1 语法错误

  其实,语法错误是可以避免的。VS 2019提供了强大的智能感知技术,要尽量利用该技术辅助书写源程序,不但可提高输入速度,还可以避免语法错误。如图7-2所示,当输入了”Convert.“时,系统会自动显示Convert类的所有成员方法,通过光标移动键查找并定位于某个方法,按空格键,即可完成相关诸如”Convert.ToInt32”之类的输入操作。

 

图 7-2 利用智能感知技术输入源代码

  2.逻辑错误

  逻辑错误通常不会引起程序本身的运行异常。因为分析和设计不充分,造成程序算法有缺陷或完全错误,这样根据错误的算法书写程序,自然不会获得预期的运行结果。因此逻辑错误的实质时算法错误,是最不容易发现的,也是最难解决的,必须重新检查程序的流程是否正确以及算法是否与要求相符,有时可能选哟逐步地调试分析,甚至还要适当地添加专门地调试分析代码来查找其出错地原因和位置。

  逻辑错误无法依靠.NET 编译器进行检查,只有依靠程序员认真、不懈的努力才能解决。正因如此,寻找新算法、排除逻辑错误才是广大程序员的价值所在。

  3.运行时错误

  运行时错误是指在应用程序试图执行系统无法执行的操作时产生的错误,如以零作除数,也就是人们所说的系统报错。这类错误编译器是无法自动检查出来的,通常需要对输入的代码进行手动检查并更正。

  【实例 7-1】设计一个Windows程序,求斐波那契数列的前十项。斐波那契数列,又称黄金分割数列,指的是这样一个数列:0,1,1,2,3,5,8,13,21,...

  【分析】斐波那契数列除第1、2项外,其余各项为其前两项之和,假设a是一维数组,则:a[i]=a[i-1]+a[i-2]。为此,可使用for语句来循环处理。核心代码如下:

 

for(int i = 2; i <= a.Length; i++)
{
    a[i] = a[i - 2] + a[i - 1];
    lblShow.Text += " " + a[i];
 }

 

  上述代码算法很简单,编译时也不会报错,但运行时会出现错误,如图 7-3所示

 

 

 

图 7-3 运行时错误

 

  显然,错误原因是索引超出数组界限,数组的长度为10,但其最大的索引值应当为9,当i=10,即a[10]越界。为此,可对for语句作如下修改:

 for(int i = 2; i <a.Length; i++)

 

可见,编程思路不严密会造成运行时错误。对于初学者来说,只有通过大量地、不懈地练习编程,才能有效解决这一问题。

  7.1.2调试程序错误

  为了更快地发现程序错误和更好地排除错误,VS 2019提供了功能强大地调试器。通过该调试器来调试程序,开发人员可以监察程序运行的具体情况,分析各变量、对象在运行期间的值和属性等。

  1.VS 2019的调试方式

  VS2019提供多种调试方式,包括逐语句方式、逐过程方式和断点方式等。

  其中,逐语句方式和逐过程方式都是逐行执行程序代码,所不同的是,当遇到方法调用时,前者将进入方法体内继续逐行执行,而后者不会进入方法体内跟踪方法本身的代码。所以如果在调试的过程中想避免执行方法体内的代码,就可以使用逐过程方式;相反,如果想查看方法体代码是否出错,就要使用逐语句方式。

  在VS 2019中,选择”调试“菜单的”逐语句“命令(如图7-4所示)或者按F11键,可启用逐语句方式,连续按F11键可跟踪每一条语句的执行。而选择”调试“菜单中的”逐过程“命令或者按F10键,可启用逐过程方式。

 

图 7-4 ”调试“菜单

  在使用逐语句方式进入方法体时,如果想立即回到调用方法的代码处,可选择”调试“菜单的”跳出“命令或者按Shift+F11键。

  在调试过程中,想要结束调试,可选择”调试“菜单中的”终止调试“命令或按Shift+F5键。

  为了更好地观察运行期地变量和对象的值,VS 2019还提供了监视窗口、自动窗口和局部变量窗口,以辅助开发人员更快地发现错误。在调试过程中,右击变量名,在快捷菜单中选择”添加监视“命令,即可将一个变量添加到监视窗口进行单独观察。

  例如,图7-5展示了在以逐语句方式调试程序时监视实例7-1的数组a的情况。其中,亮显部分代表当前正在执行的代码行,监视窗口显示了数组a各元素的详细信息。

 

 

图 7-5 监视程序的运行

 

  2.VS 2019的断点方式

 

  通过逐行执行程序来寻找错误,效果确实很好。但是,对于较大规模的程序或者已经知道错误范围的程序,使用逐语句方式或逐过程方式,都是没有必要的。为此,可使用断点方式调试程序。

 

  断点是一个标志,它通知调试器应该在某处中断应用程序并暂停执行。与逐行执行不同的是,断点方式可以让程序一直执行,直到遇到断点才开始调试。显然,这将大大加快调试过程。VS2019 允许在源程序中设置多个断点。

 

  设置断点的操作方法如下。

 

  右击想要设置断点的代码行,选择”断点“->"插入断点"命令即可;也可以单击源代码行左边的灰色区域;或者将插入点定位于想设置断点的代码行,再按F9键。如图7-6所示,断点以红色圆点表示,并且该行代码也高亮显示。

 

 图 7-6 设置断点

  注意,设置断点后,再次单击该断点,或再次按F9,将删除该断点。

  按上述方法设置的断点,默认情况下是无条件中断的,但有时不仅需要在某处中断,还要满足一定条件的前提下才发生中断。此时,可通过修改断点来设置中断条件。

  为断点设置中断条件的曹祖方法如下:

  首先右击断点,选择”断点“->"条件"命令,出现”断点条件“对话框,然后输入断点条件,单击”确定“按钮。

  例如,针对图7-6的断点,可设置断点条件为"i>a.Lenght",效果如图7-7所示。

  设置断点之后,选择”调试“->”启动调试“菜单命令,或按F5键即可进入调试过程。

图 7-7 设置断点条件

  3.人工寻找逻辑错误

  在众多的程序错误中,有些错误是很难发现的,尤其是逻辑错误,即便是功能强大的调试器也显得无能为力。这时可以适当加入一些人工操作,以便快速地找到错误。常见地方法有以下两种。

  (1)注释可能出错地代码。这是一种比较有效地寻找错误地策略。如果注释掉部分代码后,程序就能正常运行,那么就能肯定该代码出错了;反之,错误应该在别处。

  (2)适当地添加一些输出语句,再观察是否成功显示输出信息,即可判断包含该输出语句地分支和循环结构是否有逻辑错误,从而进一步分析错误的原因。

7.2 程序的异常处理

  只要程序中存在错误,不论是什么原因造成的,.NET都会引发异常。因此,异常也是C#中的一个重要概念,掌握对异常的处理同样很有必要。本节将主要介绍在C#中异常处理的一般机制和基本语法。

  7.2.1异常的概念

  一个优秀的程序员在编写程序时,不仅要关系代码正常的控制流程,同时也要把握好系统可能随时发生的不可预期的事件。它们可能来自系统本身,如内存不够、磁盘出错、网络连接中断、数据库无法使用等;也可能来自用户,如非法输入等,一旦发生这些事件,程序都将无法正常运行。

  所谓异常就是那些能影响程序正常执行的事件,而对这些事件的处理方法称为异常处理。异常处理是必不可少的,它可以防止程序处于非正常状态,并可根据不同类型的错误来执行不同的处理方法。

  【实例 7-2】设计一个Windows程序,用户输入一个整数,计算该数的阶乘。

  【分析】编程时,首先通过TextBox的Text属性(string型)提取用户输入,再使用Convert.ToInt32方法将文本字符串转换为整型,最后再进行相应计算处理。

  主要源代码如下:

 private void btnOK_Click(object sender, EventArgs e)
        {
            int num = Convert.ToInt32(textBox1.Text);
            int result = 1;
            for(int i = 1; i <= num; i++)
            {
                result *= i;
            }
            lblShow.Text = string.Format("{0}!={1}", num, result);
        }
View Code

  上述代码无论是语法还是程序逻辑,均没有错误。但是,如果用户在输入整数时,用英文单词或汉字来表示数字,则发生异常,如图7-8所示。

图 7-8 出现异常

  在本例中,造成异常的原因是:TextBox控件本身不具备限制用户输入的功能,设计人员又按常规进行设计,但当用户不按常规输入数据时,系统自然发生异常。

  所以,如果不想让程序因出现异常而被系统中断或退出,必须构建相应的异常处理机制。

  在开发应用程序的过程中,可以假定任何代码块都有可能引发异常,特别是CLR本身可能引发的异常,例如溢出、数组越界、除数为0等。为了能够对异常有效处理,C#提供了try、catch、finally关键字来处理可能有异常的操作,其格式一般如下:

try
{
    语句块1        //可能引发异常的代码
}
catch(异常对象)    //捕获异常类对象
{
    语句块2           //实现异常处理
}
finally
{
     语句块3          //无论是否异常,都作最后处理
}

其中:

  (1)try块包含的代码组成程序的正常操作部分,但可能会遇到某些严重的错误情况。

  (2)catch块包含的代码用于处理各种错误情况,这些错误是在try块中的代码执行时遇到的,catch块可以有多个,用于捕获不同类型的异常。

  (3)finally块包含的代码清理资源或执行需要无论有无异常产生,都需要执行的操作。

  try...catch..finally语句的执行流程如图7-9(a)所示,具体过程如下。

 图 7-9 异常处理流程图

  (1)程序执行try语句块。

  (2)如果执行try语句块时发生异常,则由系统自动捕获并将相关信息封装保存到”异常对象“之中,如果该异常的类型于catch后的异常对象一致,则执行(4),如果与catch后的异常对象不一致,则继续抛出异常,并由后面的catch来处理该异常。

  (3)如果没有发生异常,则执行(5)。

  (4)执行catch块,实现异常处理,然后执行(5)。

  (5)执行finally块。

  其中,”异常对象“是Exception类或其派生类的实例。该对象的Message属性可返回异常信息。公共语言运行时CLR预定义了多种异常类,在异常处理时可直接使用,如表7-1所示。

表7-1 常用系统异常类
异常类 说明
AccessViolationException 在试图读写受保护的内存时引发的异常
ApplicationException 发生非致命应用程序错误时引发的异常
ArithmeticException 因算术运算、类型转换或转换操作时引发的异常
DivideByZeroException 试图用零除整数值或十进制数值引发的异常
FieldAccessException 试图非法访问类中的私有字段或受保护字段时引发的异常
IndexOutoRangeException 试图访问索引超出数组界限的数值时引发的异常
NotSupportedException 当调用的方法不受支持时引发的异常
InvalidCastException 因无效类型转换或显式转换时引发的异常
NullReferenceException 尝试取消引用空对象时引发的异常
OutOfMemoryException 没有足够的内存继续执行应用程序时引发的异常
OverFlowException 在选中的上下文所执行的操作不能加载它时引发的异常
FileLoadException 当找到托管程序集却不能加载它时引发的异常
FileNotFoundException 尝试访问磁盘上不存在的文件失败时引发的异常

  .NET中有两个重要的类,它们都派生于System.Exception。

  (1)System.SystemException:通常由.NET运行时引发,所有未经过处理的基于.NET的应用程序的错误都是由此引发,表7-1所列出的异常类都是由SystemException类派生。

  (2)ApplicationException:如果需要自定义一个异常类,可以由ApplicationException类派生。

  System.Eception是一个很常用的异常处理类,它的几个比较常用的属性如表7-2所示。

表 7-2 System.Exception类常用属性
异常类 说明
HelpLink 链接到一个帮助文件上,以提供该异常的更多信息
Message 描述错误情况的文本
Source 导致异常的应用程序或对象名
StackTrace 堆栈上方法调用的信息,它有助于跟踪引发异常的方法
TargetSite 引发异常的方法的.NET反射对象
InnerException 如果异常是在catch块中引发的,它就会包含把代码发送到catch块中的异常对象

  try、catch、finally关键字的组合方式如下:

  try...catch

  try...finally

  try...catch...finally

   7.2.3 try...catch语句

  try..catch语句由一个try块后跟一个或多个catch子句构成,语法如下:

try
{
    语句块1            //可能引发异常的代码
}
catch(异常对象1)    //捕获异常类对象
{
    语句块2            //实现异常处理
}
catch(异常对象2)    //捕获异常类对象
{
    语句块3           //实现异常处理
}
...

流程图如图7-9(b)和图7-9(c)所示。

【实例 7-3】修改实例7-2,添加异常处理功能。

可用try...catch来处理,主要代码如下:

 private void btnOK_Click(object sender, EventArgs e)
        {
            try
            {
                int num = Convert.ToInt32(textBox1.Text);
                int result = 1;
                for (int i = 1; i <= num; i++)
                {
                    result *= i;
                }
                lblShow.Text = string.Format("{0}!={1}", num, result);
            }
            catch(SystemException exc)
            {
                lblShow.Text = "产生异常:" + exc.Message;
            }
            

        }
View Code

  运行上述代码,输入five数据,结果不但不再出现中断和系统报错,而且还能输出异常信息,如图7-10所示。

 

图 7-10 异常处理示例

 

   使用try...catch语句时,特别要注意以下两点。

  (1)catch子句中的异常对象可以省略。如果省略异常对象,则默认未CLR的异常类对象,否则为指定的异常来的对象。例如:

catch
{
    lblShow.Text="产生异常!";
}

  (2)由于try子句中代码有可能引发不止一种异常,因此C#允许针对不同的异常,定义多个不同的catch子句。当try子句抛出异常时,系统将根据异常类型顺序查找并执行对应的catch子句,实现特定异常处理,例如:

        try
            {
                int num = Convert.ToInt32(textBox1.Text);
                int result = 1;
                for (int i = 1; i <= num; i++)
                {
                    result *= i;
                }
                lblShow.Text = string.Format("{0}!={1}", num, result);
            }
            catch(System.FormatException exc)
            {
                lblShow.Text = "产生异常:" + exc.Message;
            }
            catch (System.OverflowException exc)
            {
                lblShow.Text = "产生异常:" + exc.Message;
            }
            catch (System.Exception exc)
            {
                lblShow.Text = "产生异常:" + exc.Message;
            }

  当有多个catch块时,如果try中产生异常,将按catch块的顺序检查每个catch块的捕获类型,如果产生异常的类型和其中一个catch块的捕获类型一致,则执行该catch块,并忽略其他catch块,所以catch块的顺很重要,应该将捕获特定程度较高的异常放在前面,捕获范围广的异常放在后面,如上例中,如果将catch(System.Exception exc)放在catch(System.FormatException exc)和catch (System.OverflowException exc)的前面,则编译将出错,因为System.Exception包含System.FormatException和System.OverflowException类型的异常,则后面两个异常将永远无法执行。

  7.2.4 finally语句

  在try...catch语句中,只有捕获到了异常,才会执行catch子句中的代码。但还有一些比较特殊的操作,比如文件的关闭、网络连接的断开以及数据库操作中锁的释放等,应该是无论是否发生异常都必须执行,否则会造成系统资源的占用和不必要的浪费。类似这些无论是否捕捉到异常都必须执行的代码,可用finally关键字定义。finally语句常常与try...catch语句搭配使用。例如在上例中增加一个finally语句,用于表示无论是否有异常都要执行的内容。

            try
            {
                int num = Convert.ToInt32(textBox1.Text);
                int result = 1;
                for (int i = 1; i <= num; i++)
                {
                    result *= i;
                }
                lblShow.Text = string.Format("{0}!={1}", num, result);
            }
            catch (System.Exception exc)
            {
                lblShow.Text = "产生异常:" + exc.Message;
            }
            finally
            {
                lblShow.Text += "\n\nfinally:执行完毕!\n";
            }

  该程序运行效果如图7-11所示,无论有无异常,都会显示”finally:执行完毕!“,这表明程序执行了finally语句。

         

 

图 7-11 finally运行结果

  7.2.5 throw语句与抛出异常

  前面所捕获到的异常,都是当遇到错误时,系统自己报错,自动通知运行环境异常的发生。但是有时还可以在代码中手动地告知运行环境在什么时候发生了什么异常。C#提供的throw语句可手动抛出一个异常,使用格式如下:

  throw[异常对象]    //提供有关抛出的异常信息

  当省略异常对象时,该语句只能用在catch语句中,用于再次引发异常处理。

  当throw语句带有异常对象时,则抛出指定的异常类,并显示异常的相关信息。该异常既可以是预定义的异常类,也可以是自定义的异常类。

  【实例 7-4】修改实例7-3,自定义一个异常类MyException,封装异常信息”警告!输入的整数只能在0到16之间!“,以增强异常检测与处理。

class MyException : Exception               //自定义异常类
        {
            public MyException(string str1) : base(str1) { }
            public MyException(string str1, Exception e) : base(str1, e) { }
        }
        private void btnOK_Click(object sender, EventArgs e)
        {
            try
            {
                int num = Convert.ToInt32(textBox1.Text);
                if (num < 0 || num > 16)                                    //如果num没有在0到16之间,则抛出异常
                    throw new MyException("警告!输入的整数只能在0到16之间!");
                int result = 1;
                for (int i = 1; i <= num; i++)
                {
                    result *= i;
                }
                lblShow.Text = string.Format("{0}!={1}", num, result);
            }
            catch(MyException exc)
            {
                lblShow.Text = "产生异常:" + exc.Message;           //输出自定义的异常信息
            }
            catch (System.Exception exc)
            {
                lblShow.Text = "产生异常:" + exc.Message;
            }
            finally
            {
                lblShow.Text += "\n\nfinally:执行完毕!\n";
            }

        }
View Code

   该程序运行效果如图 7-12所示。

 

 

 图 7-12 自定义异常处理

 【课后实例】设计一个Windos应用程序,在一个文本框中输入n个数字,中间用逗号作间隔,然后编程对数字排序并输出,效果如图7-13所示。

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace 第七章
{
    public partial class 课后实例 : Form
    {
        public 课后实例()
        {
            InitializeComponent();
        }

        private void button1_Click(object sender, EventArgs e)
        {
            //将输入的数字系列以逗号为分隔符,划分为字符串数组
            string[] sources = textBox1.Text.Split('');
            int[] a = new int[sources.Length];
            try
            {
                for (int i = 0; i < sources.Length; i++)
                {
                    a[i] = Convert.ToInt32(sources[i]);             //将字符串数组中的每个数字转换为整数
                }
            }
            catch(Exception ex)
            {
                lblShow.Text = ex.Message;
            }
           
            for(int i = 1; i <= a.Length; i++)                  //冒泡排序
            {
                for(int j=i;j<=a.Length-i;j++)
                {
                    if(a[j-1]>a[j])
                    {
                        int t = a[j - 1];
                        a[j - 1] = a[j];
                        a[j] = t;
                    }
                }
            }
            lblShow.Text = "排序后的序列是:";
            foreach(int t in a)                     //依次输出排序后的元素
            {
                lblShow.Text += String.Format("{0,-4:D}", t);
            }
        }
    }
}
View Code