第3章 流程控制语句
上一章介绍了C#编程的基础知识,利用这些知识已经可以编写一些基本的C#语句(如声明语句和赋值语句等),并且能够实现控制台输入输出。如果不控制流程,程序将从头到尾执行每个语句。仅仅使用顺序结构只能编写一些很简单的程序。
但是任何编程语言的许多功能都需要使用条件语句和迭代更改执行顺序,通过流程控制语句可以控制程序执行的流程,从而编写制定决策或重复操作的代码。本章介绍C#语言中的流程控制语句,主要内容包括选择语句、迭代语句、跳转语句、异常处理语句、程序调试,以及预编译处理等。
3.1 选择语句
选择语句会根据表达式的值从若干个给定的语句中选择一个来执行,在C#语言中选择语句包括if语句和switch语句。
3.1.1 if语句
if语句根据布尔表达式的值选择要执行的语句,其基本语法格式如下:
if (boolean-expression) { embedded-statement; }
当执行上述语句时,首先计算布尔表达式boolean-expression的值。如果为true,则执行嵌入语句embedded-statement;如果为false,则将忽略嵌入语句。其中嵌入语句可以是单条语句或多条语句块,单条语句可以省略花括号;多条语句必须使用花括号。
根据需要,也可以在一个if语句中嵌入另一个if语句。
若要在满足某个条件时执行选择一个语句,而在不满足该条件时执行另一个语句,可以使用关键字else来扩展if语句,语法如下:
if (boolean-expression) { embedded-statement-1; } else { embedded-statement-2; }
当执行上述语句时,首先计算布尔表达式boolean-expression的值。如果为true,则执行语句嵌入语句embedded-statement-1;否则将执行嵌入语句embedded-statement-1。其中嵌入语句可以是单条语句或多条语句。
若要同时处理多个条件,还可以使用else-if来扩展if语句,其语法如下:
if (boolean-expression-1){ embedded-statement-1} else if (boolean-expression-2){ embedded-statement-2;} else if (boolean-expression-3){ embedded-statement-3;} else{ embedded-statement-n;}
当执行上述语句时,首先计算布尔表达式boolean-expression-1的值。如果为true,则执行嵌入语句embedded-statement-1;否则计算布尔表达式boolean-expression-2的值。如果为true,则执行嵌入语句embedded-statement-2,否则计算布尔表达式boolean-expression-3的值,依此类推。如果所有布尔表达式的值都是false,则执行嵌入语句embedded-statement-n,其中各个嵌入语句可以是单条语句或多条语句。
【例3-1】创建一个C#控制台应用程序,说明如何使用if语句来检查输入的字符是否为小写字母、大写字母或数字,程序运行结果如图3-1所示。
图3-1 程序运行结果
【设计步骤】
(1)在Visual C#中创建一个控制台应用程序项目,项目名称为“ConsoleApplication3-01”。解决方案名称为“chapter03”,保存在F:\Visual C sharp 2008\chapter03文件夹中。
(2)在代码编辑器中打开源文件Program.cs,然后在Main方法中输入以下代码:
Console.Title = "if语句应用示例"; Console.Write("请输入一个字符:"); char ch = (char)Console.Read(); // 在.NET Framework中,使用Char结构表示Unicode字符 if (Char.IsUpper(ch)) // Char.IsUpper方法指示某个Unicode字符是否属于大写字母 { Console.WriteLine("这是一个大写字母。"); } else if (Char.IsLower(ch)) // Char.IsLower方法指示某个Unicode字符是否属于小写字母 { Console.WriteLine("这是一个小写字母。"); } else if (Char.IsDigit(ch)) // Char.IsDigit方法指示某个Unicode字符是否属于十进制数字 { Console.WriteLine("这是一个数字。"); } else { Console.WriteLine("这个字符不是字母,也不是数字。"); }
(3)保存文件,连续4次运行程序并输入不同内容测试。
● 第1次运行时,输入一个小写字母。
● 第2次运行时,输入一个大写字母。
● 第3次运行时,输入一个数字。
● 第4次运行时,输入一个“#”字符。
3.1.2 switch语句
若要将同一个变量或表达式与多个不同的值进行比较,并根据其值来执行不同的代码,可以使用switch语句。该语句通过将控制传递给其体内的一个case语句来处理多个选择和枚举,语法格式如下:
switch (expression) { case constant-expression-1: embedded-statement-1; break; case constant-expression-2: embedded-statement-2; break; case constant-expression-3: embedded-statement-3; break; default: embedded-statement-n; break; }
其中位于括号中的表达式expression称为“switch表达式”,该表达式的类型称为“switch语句的主导类型”,可以是sbyte、byte、short、ushort、int、uint、long、ulong、char、string或枚举类型;constant-expression-1和constant-expression-2等是常量表达式,称为“case表达式”,case关键字、case表达式和冒号组成一个case标签。
执行switch语句时,首先计算switch表达式的值(称为“开关值”)。若等于第1个case标签中的常量值,则执行嵌入语句embedded-statement-1,直到遇到第1个break语句或switch结构结束;否则检查开关值是否等于第2个case标签中的常量值,若相等,则执行语句嵌入语句embedded-statement-2。直到遇到第2个break语句或switch结构结束,依此类推。如果没有任何case表达式与开关值匹配,则控制传递给可选default标签后的嵌入语句embedded-statement-n,然后结束switch结构。如果没有default标签,则控制传递到switch以外。
在switch语句的执行过程中,控制传递给与开关的值匹配的case语句。switch语句可以包括任意数目的case实例,不过任何两个case表达式都不能具有相同的值。语句体从选定的语句开始执行,直到break将控制传递到case体以外。在每一个case块(包括上一个块,不论其是case语句,还是default语句)的后面都必须有一个跳转语句(如break)。C#不支持从一个case标签显式贯穿到另一个case标签,但有一个例外,即case语句中没有代码时。
【例3-2】创建一个C#控制台应用程序,说明如何使用switch语句根据输入的年份来计算出属相,程序运行结果如图3-2所示。
图3-2 程序运行结果
【设计步骤】
(1)在解决方案chapter03中添加一个控制台应用程序项目,项目名称为“ConsoleApplication3-02”,保存在F:\Visual C sharp 2008\chapter03文件夹中,将该项目设置为启动项目。
(2)在代码编辑器中打开源文件Program.cs,然后在Main方法中输入以下代码:
Console.Title = "switch语句应用示例"; Console.Write("请输入您的出生年份:"); string str = Console.ReadLine(); int year = int.Parse(str); string animalSign = ""; switch (year % 12) { case 0: animalSign = "猴"; break; case 1: animalSign = "鸡"; break; case 2: animalSign = "狗"; break; case 3: animalSign = "猪"; break; case 4: animalSign = "鼠"; break; case 5: animalSign = "牛"; break; case 6: animalSign = "虎"; break; case 7: animalSign = "兔"; break; case 8: animalSign = "龙"; break; case 9: animalSign = "蛇"; break; case 10: animalSign = "马"; break; case 11: animalSign = "羊"; break; } Console.WriteLine("您的属相是:{0}\n", animalSign);
(3)按Ctrl+F5组合键编译并运行应用程序,然后输入一个年份,由程序计算出属相。
3.2 迭代语句
迭代语句用于创建迭代结构,以重复执行嵌入语句。使用迭代语句可以根据迭代终止条件来多次执行嵌入的语句,除非遇到跳转语句;否则这些语句将按顺序执行。在C#语言中有多种形式的迭代语句,如while、do-while、for,以及foreach语句。
3.2.1 while语句
while语句执行一个语句或语句块,直到指定的表达式计算为false,语法格式如下。
while (boolean-expression) { embedded-statement; }
当执行while语句时,首先计算布尔表达式boolean-expression的值。只要该表达式的值为true,则重复执行嵌入语句embedded-statement(也称为“迭代体”),直到该表达式值变为false时结束迭代。在某些情况下,表达式boolean-expression的值开始就是false,此时迭代语句一次都不会执行。嵌入语句可以是单个语句或多条语句,如果是单个语句,也可以省略花括号。
使用while语句时,应注意以下几点。
(1)由于while表达式的测试在每次执行迭代前发生,因此while迭代执行零次或多次。
(2)当break、goto、return或throw语句将控制权转移到while迭代之外时可以终止该迭代。
(3)若要将控制权传递给下一次迭代,但不退出迭代,可以使用continue语句。
(4)如果通过测试某个变量的值来决定是否继续迭代,则应注意在迭代体中修改该变量的值;否则可能会陷入死迭代。
【例3-3】创建一个C#控制台应用程序,用于计算并显示所有的水仙花数,程序运行结果如图3-3所示。水仙花数指一个3位数,其各位数字的立方和等于该数。例如,153是一个水仙花数,因为153=13+53+33。
图3-3 程序运行结果
【设计步骤】
(1)在解决方案chapter03中添加一个控制台应用程序项目,项目名称为“ConsoleApplication3-03”。保存在F:\Visual C sharp 2008\chapter03文件夹中,将该项目设置为启动项目。
(2)在代码编辑器中打开源文件Program.cs,然后在Main方法中输入以下代码:
int i = 100; int a, b, c; Console.Title = "while语句应用示例"; Console.WriteLine("水仙花数如下:"); while (i < 1000) { a = (int)(i / 100); b = (int)(i - a * 100) / 10; c = i - a * 100- b * 10; if (i == a * a * a + b * b * b + c * c * c) { Console.Write("{0}\t", i); } i++; } Console.WriteLine("\n");
(3)按Ctrl+F5组合键编译并运行应用程序。
3.2.2 do语句
do语句也称为“do-while迭代”,该语句重复执行括嵌入语句,直到指定的表达式计算为false,其语法格式如下。
do { embedded-statement; } while (boolean-expression);
执行do-while迭代时,首先执行一次嵌入语句embedded-statement(也称为“迭代体”),然后计算布尔表达式boolean-expression的值;如果布尔表达式的值为true,则重复执行嵌入语句embedded-statement,直至布尔表达式的值变为false时结束迭代,从do-while迭代后的第1个语句执行。
使用do语句时注意以下几点。
● 与while语句不同,do-while迭代会在计算条件表达式之前执行一次。因此do-while迭代将执行一次或多次。
● 在do-while块中的任何位置都可以使用break语句跳出迭代。
● 当在迭代体中使用continue语句时,可以直接计算布尔表达式boolean-expression;如果计算结果为true,则会继续从迭代体的第1个语句执行;如果计算结果为false,则会继续从do-while迭代后的第1个语句执行。
● do-while迭代还可以通过goto、return或throw语句退出。
【例3-4】创建一个C#控制台应用程序,使用do-while迭代编写一个简单的打字练习程序。可以输入一行或多行字符,按q或Q键退出练习,程序运行结果如图3-4所示。
图3-4 程序运行结果
【设计步骤】
(1)在解决方案chapter03中添加一个控制台应用程序项目,项目名称为“ConsoleApplication3-04”。保存在F:\Visual C sharp 2008\chapter03文件夹中,将该项目设置为启动项目。
(2)在代码编辑器中打开源文件Program.cs,然后在Main方法中输入以下代码:
Console.Title = "do语句应用示例"; Console.WriteLine("欢迎使用打字练习程序(Q=退出):"); char ch; string str = ""; do { ch = (char)Console.Read(); str += ch; } while (ch != 'Q' && ch != 'q'); Console.WriteLine("您输入的内容是:\n{0}\n", str.Substring(0, str.Length -1));
(3)按Ctrl+F5组合键编译并运行应用程序。
3.2.3 for语句
for迭代重复执行一个语句或语句块,直到指定的表达式计算为false值,其语法如下:
for (for-initializer; for-condition; for-iterator) { embedded-statement; }
其中for-initializer、for-condition和for-iterator都是可选项;for-initializer初始化迭代控制变量,迭代控制变量可以有一个或多个(用逗号分开);for-condition为布尔表达式,用于指定迭代控制条件;表达式for-iterator按规律(如递增或递减)改变迭代控制变量的值;嵌入语句embedded-statement可以是单个或多个语句,如果是单个语句,可以省略花括号。
for语句的执行流程如下。
(1)计算表达式for-initializer,初始化迭代变量。
(2)计算布尔表达式for-condition,若其值为false,则结束for迭代;否则继续执行。
(3)执行嵌入语句embedded-statement。
(4)计算表达式for-iterator,修改迭代变量。
(5)转到步骤(2)。
使用for语句时,应注意以下几点。
(1)由于条件表达式的测试发生在迭代执行之前,因此for语句可能执行零次或多次。
(2)用for-initializer声明的变量是局部变量,其范围从该变量声明开始。一直延伸到嵌入语句的结尾,该范围包括forcondition和foriterator。
(3)for-initializer和for-iterator可以包含多个表达式,表达式之间用逗号分开。
(4)如果在for语句之前已为迭代变量赋值,则可以省略for-initializer,但分号不能省略。
(5)在for语句中可以省略布尔表达式for-condition,但分号不能省略。此时不判断迭代控制条件,相当于布尔表达式恒为true。因此会构成无限迭代,应在迭代体中添加跳出迭代的控制语句。
(6)在for语句中可以省略表达式for-iterator,在这种情况下应在迭代体中添加改变迭代控制变量值的语句;否则会形成无限迭代。
(7)如果在for语句中同时省略for-initializer和for-iterator,则其功能相当于一个while语句。
(8)for语句中的所有表达式都是可选的,可以省略所有表达式,for( ; ; )相当于while(true)。使用这种形式的for语句可以构成一个无限迭代,此时应在迭代体中添加跳出迭代的控制语句。
(9)for语句可以嵌套使用,即在一个for语句的迭代体内可以嵌套另一个完整的for语句,也可以嵌套其他形式的迭代语句。
【例3-5】创建一个C#控制台应用程序,使用嵌套的for迭代显示一个由星号组成的直角三角形,程序运行结果如图3-5所示。
图3-5 程序运行结果
【设计步骤】
(1)在解决方案chapter03中添加一个控制台应用程序项目,项目名称为“ConsoleApplication3-05”。保存在F:\Visual C sharp 2008\chapter03文件夹中,将该项目设置为启动项目。
(2)在代码编辑器中打开源文件Program.cs,然后在Main方法中输入以下代码:
Console.Title = "for语句应用示例"; for (int i = 0; i < 10; i++) { for (int j = 0; j < i; j++) { Console.Write("* "); } Console.WriteLine(); }
(3)按Ctrl+F5组合键编译并运行应用程序。
3.2.4 foreach语句
foreach语句对数组或对象集合中的每个元素重复执行嵌入语句,语法如下:
foreach(type element in collection) { embedded-statement; }
其中type指定数组或集合中元素的数据类型;element为迭代变量,也称为“迭代变量”,用于依次获取数组或集合中的每一个元素;collection表示一个数组或集合。
foreach语句的执行流程如下。
(1)计算集合表达式collection的值并生成一种集合类型的实例。
(2)调用集合的GetEnumerator方法得到一个枚举实例的值,返回的枚举值将存放在一个临时局部变量中。
(3)调用临时局部变量的MoveNext方法,以获取下一个枚举元素。
(4)如果MoveNext方法的返回值为false,则表明已为集合中的所有元素完成迭代,控制传递给foreach块之后的下一个语句;否则继续执行。
(5)计算临时局部变量的Current属性以获取当前枚举值并将其转换为foreach语句中规定的变量类型,并将结果存储在迭代变量中,以便在迭代体内可以访问该变量的值。
(6)执行迭代体包含的语句,然后转到步骤(3)开始下一轮迭代。
使用foreach语句时,应注意以下几点。
(1)在foreach语句声明的变量是一个局部变量,只能在迭代体中使用,而且不能为其重新赋值。
(2)在foreach迭代体的任何位置可以使用break语句跳出迭代,或使用continue语句进入迭代的下一轮迭代。
(3)foreach迭代也可以通过goto、return或throw语句退出。
foreach按以下顺序来遍历数组的元素,对于一维数组,按递增的索引顺序遍历元素。从索引0开始,到索引Length-1结束;对于多维数组,首先增加最右边维度的索引,然后是其左边紧邻的维度。依此类推,直到最左边的那个维度。
【例3-6】创建一个C#控制台应用程序,使用foreach迭代显示每个数组元素并计算其和。进而计算出数组元素的平均值,程序运行结果如图3-6所示。
图3-6 程序运行结果
【设计步骤】
(1)在解决方案chapter03中添加一个控制台应用程序项目,项目名称为“ConsoleApplication3-06”。保存在F:\Visual C sharp 2008\chapter03文件夹中,将该项目设置为启动项目。
(2)在代码编辑器中打开源文件Program.cs,然后在Main方法中输入以下代码:
Console.Title = "foreach语句应用示例"; int sum = 0; int[] array = new int[] {3, 6, 8, 9, 12, 16, 19, 26, 38, 56}; Console.Write("数组元素为:"); foreach (int element in array) { Console.Write("{0} ", element); sum += element; } Console.WriteLine(); Console.WriteLine("数组元素的和为:{0}", sum); Console.WriteLine("数组元素的平均值为:{0}", (double)sum/array.Length);
(3)按Ctrl+F5组合键编译并运行应用程序。
【例3-7】创建一个C#控制台应用程序,说明如何使用foreach迭代来处理集合。创建一个哈希表并添加一些元素,然后使用foreach迭代来显示这些元素的键和值,程序运行结果如图3-7所示。
图3-7 程序运行结果
【设计步骤】
(1)在解决方案chapter03中添加一个控制台应用程序项目,项目名称为“ConsoleApplication3-07”。保存在F:\Visual C sharp 2008\chapter03文件夹中,将该项目设置为启动项目。
(2)在代码编辑器中打开源文件Program.cs,在程序的using节中添加以下using指令,以导入System.Collections命名空间:
using System.Collections;
(3)在Main方法中输入以下代码:
Console.Title = "用foreach语句处理集合"; // 使用Hashtable类创建一个哈希表对象,Hashtable类位于命名空间System.Collections中 Hashtable cityHash = new Hashtable(); // 调用Add方法将带有指定键和值的元素添加到哈希表中 cityHash.Add("010", "北京"); cityHash.Add("021", "上海"); cityHash.Add("022", "天津"); Console.WriteLine("电话区号\t城市"); foreach (string telephoneDistrictNumber in cityHash.Keys) { Console.WriteLine("{0}\t\t{1}", telephoneDistrictNumber, cityHash[telephoneDistrictNumber]); } Console.WriteLine();
(4)按Ctrl+F5快捷键编译并运行应用程序。
3.3 跳转语句
跳转语句用于无条件地转移控制,它会将控制转到某个位置,即其目标。在C#语言中,跳转语句包括break、continue、goto和return语句。
3.3.1 break语句
break语句用于退出直接封闭它的switch、while、do、for或foreach语句,控制传递给终止语句后面的语句(如果有的话),其语法如下:
break;
在执行次数有限的迭代中,使用break语句可以提前结束迭代;在无限迭代中,通常使用break语句来跳转到这个迭代语句之外。
当多个switch、while、do、for或foreach语句彼此嵌套时,break语句只应用于最里层的语句。若要穿越多个嵌套层转移控制,必须使用goto语句。
【例3-8】创建一个C#控制台应用程序,说明如何使用break语句跳到迭代外面,程序运行结果如图3-8所示。
图3-8 程序运行结果
【设计步骤】
(1)在解决方案chapter03中添加一个控制台应用程序项目,项目名称为“ConsoleApplication3-08”。保存在F:\Visual C sharp 2008\chapter03文件夹中,将该项目设置为启动项目。
(2)在代码编辑器中打开源文件Program.cs,然后在Main方法中输入以下代码:
Console.Title = "break语句应用示例"; for (int i = 1; i <= 999; i++) { Console.Write("{0} ", i); if (i == 10) break; } Console.WriteLine("\n");
(3)按Ctrl+F5组合键编译并运行应用程序。
3.3.2 continue语句
continue语句将控制权传递给它所在的封闭迭代语句的下一次迭代,语法如下:
continue;
与break语句一样,continue语句也可以改变迭代语句的执行流程。该语句将控制传递到当前迭代的条件判断部分。以执行下一轮迭代,而不再执行迭代体中剩余的其他语句。与break语句不同的是,它仅终止当前这一轮迭代的执行。
【例3-9】创建一个C#控制台应用程序,说明如何使用continue语句提前结束当前一轮迭代,程序运行结果如图3-9所示。
图3-9 程序运行结果
【设计步骤】
(1)在解决方案chapter03中添加一个控制台应用程序项目,项目名称为“ConsoleApplication3-09”。保存在F:\Visual C sharp 2008\chapter03文件夹中,将该项目设置为启动项目。
(2)在代码编辑器中打开源文件Program.cs,然后在Main方法中输入以下代码:
Console.Title = "continue语句应用示例"; for (int i = 1; i <= 1000; i++) { if (i < 996) continue; Console.Write("{0} ",i); } Console.WriteLine();
(3)按Ctrl+F5组合键编译并运行应用程序。
3.3.3 goto语句
goto语句将程序控制直接传递给标记语句,要求标记语句与其处在同一个函数中。该语句有以下3种语法格式:
goto identifier; goto case constant-expression; goto default;
goto identifier语句的目标是具有给定标签的标记语句,如果当前函数成员中不存在具有给定名称的标签或者goto语句不在该标签的范围内,则会发生编译时错误。此规则允许使用goto语句将控制转移出嵌套范围,但是不允许转移进嵌套范围。
goto case语句的目标是直接封闭其switch语句中的语句列表,switch语句必须包含一个具有给定常量值的case标签。如果goto case语句不是由switch语句封闭的,或者constant-expression不能隐式转换为直接封闭它的switch语句的主导类型,或者直接封闭它的switch语句不包含具有给定常量值的case标签,则发生编译时错误。
goto default语句的目标是直接封闭其switch语句中的语句列表,在switch语句中必须包含一个default标签。如果goto default语句不是由switch语句封闭的,或者如果直接封闭其switch语句不包含default标签,则发生编译时错误。
goto通常可以用于将控制传递给特定的switch-case标签或switch语句中的默认标签,也可以用于跳出深嵌套迭代。
【例3-10】创建一个C#控制台应用程序,说明如何使用goto语句实现迭代以计算前100个自然数之和,程序运行结果如图3-10所示。
图3-10 程序运行结果
【设计步骤】
(1)在解决方案chapter03中添加一个控制台应用程序项目,项目名称为“ConsoleApplication3-10”。保存在F:\Visual C sharp 2008\chapter03文件夹中,将该项目设置为启动项目。
(2)在代码编辑器中打开源文件Program.cs,然后在Main方法中输入以下代码:
Console.Title = "goto语句应用示例"; int i = 1, sum = 0; loop: if (i <= 100) { sum += i; i++; goto loop; } Console.WriteLine("1 + 2 + 3 + ... + 100 = {0}", sum);
(3)按Ctrl+F5组合键编译并运行应用程序。
3.3.4 return语句
return语句将控制返回到出现该语句的函数成员的调用方,其语法如下:
return expression;
其中expression是一个表达式,用于设置函数成员的返回值。
不带表达式的return语句只能用在不计算值的函数成员中,即只能用在返回类型为void的方法、属性或索引器的set访问器、事件的add和remove访问器、实例构造函数、静态构造函数或析构函数中。
带表达式的return语句只能用在计算值的函数成员中,即返回类型为非void的方法、属性或索引器的get访问器或用户定义的运算符。必须存在一个隐式转换,可将该表达式的类型转换为包含其函数成员的返回类型。
如果把return语句用在finally块中,则会导致编译时错误。
【例3-11】创建一个C#控制台应用程序,说明如何使用return语句把控制返回到函数成员的调用方,以及如何设置函数成员的返回值,程序运行结果如图3-11所示。
图3-11 程序运行结果
【设计步骤】
(1)在解决方案chapter03中添加一个控制台应用程序项目,项目名称为“ConsoleApplication3-11”。保存在F:\Visual C sharp 2008\chapter03文件夹中,将该项目设置为启动项目。
(2)在代码编辑器中打开源文件Program.cs,在Main方法定义上方声明两个静态方法:
static void SetColor(ConsoleColor backColor, ConsoleColor foreColor) { Console.BackgroundColor = backColor; Console.ForegroundColor = foreColor; Console.Clear(); return; } static double CalculateArea(double a, double b) { double area = a * b; return area; }
(3)在Main方法中输入以下代码:
Console.Title = "return语句应用示例"; SetColor(ConsoleColor.Blue, ConsoleColor.White); double a = 11.2, b = 16.98; double area = CalculateArea(a, b); Console.WriteLine("当边长a={0},b={1}时:", a, b); Console.WriteLine("矩形的面积area={0}", area);
(4)按Ctrl+F5组合键编译并运行应用程序。
3.4 异常处理语句
在C#语言中异常是一种结构化、统一和类型安全的处理机制,用于处理系统级和应用程序级的错误状态,以及程序运行时出现的任何意外或异常情况。异常可以由公共语言运行库(CLR)、第三方库或使用throw关键字的应用程序代码生成,异常处理使用try、catch和finally关键字来尝试可能未成功的操作、处理失败并在处理后清理资源。
3.4.1 异常处理概述
在C#语言中程序的运行时错误通过异常机制在程序中传播,异常由遇到错误的代码引发,由能够更正错误的代码捕捉。异常可以由 .NET Framework公共语言运行库或程序中的代码引发。一旦引发了一个异常,这个异常就会在调用堆栈中往上传播,直到找到针对它的catch语句。未捕获的异常由系统提供的通用异常处理程序处理,该处理程序会显示一个对话框。
1. 导致异常的原因
异常可以通过以下两种方式引发。
(1)throw语句用于立即无条件地引发异常,控制永远不会到达紧跟在其后的语句。
(2)在执行C#语句和表达式的过程中,有时会出现一些例外情况。从而使得某些操作无法正常完成,此时就会引发一个异常。例如,在整数除法运算中,如果分母为零,则会引发一个异常。即System.DivideByZeroException,如图3-12所示。
图3-12 System.DivideByZeroException
2. System.Exception类
System.Exception类是所有异常的基类型,此类具有一些所有异常共享的属性。
(1)Message:string类型的一个只读属性,包含关于所发生异常原因的描述。
(2)InnerException:Exception类型的一个只读属性。如果其值不是null,则其所引用的是导致当前异常的异常,即表示当前异常是在处理InnerException的catch块中被引发的;否则表示该异常不是由另一个异常引发的,以这种方式链接在一起的异常对象的数目可以为任意个。
3. 常用异常类
C#提供了多个异常类,这些异常类都是System.Exception类型或从此类派生的类型,使用这些类型可以捕获相应的异常并有针对性地处理。System命名空间中的常用异常类如下。
(1)SystemException:为System命名空间中的预定义异常定义基类。
(2)ArgumentException:在为方法提供的一个参数无效时引发的异常。
(3)ArgumentNullException:当将空引用传递给不接受其作为有效参数的方法时引发的异常。
(4)ArgumentOutOfRangeException:当参数值超出调用方法所允许取值范围时引发的异常。
(5)ArithmeticException:因算术运算、类型转换或转换操作中的错误而引发的异常。
(6)ArrayTypeMismatchException:当试图在数组中存储类型不正确的元素时引发的异常。
(7)BadImageFormatException:当DLL或可执行程序的文件图像无效时引发的异常。
(8)DivideByZeroException:试图用零除整数值或十进制数值时引发的异常。
(9)FieldAccessException:当试图非法访问类中的私有字段或受保护字段时引发的异常。
(10)FormatException:当参数格式不符合调用方法的参数规范时引发的异常。
(11)IndexOutOfRangeException:试图访问索引超出数组界限的数组元素时引发的异常。
(12)InvalidCastException:因无效类型转换或显式转换引发的异常。
(13)InvalidOperationException:当方法调用对于对象的当前状态无效时引发的异常。
(14)MethodAccessException:非法尝试访问类中的私有方法或受保护的方法时引发的异常。
(15)MissingFieldException:试图动态访问不存在的字段时引发的异常。
(16)MissingMethodException:试图动态访问不存在的方法时引发的异常。
(17)MissingMemberException:试图动态访问不存在的类成员时引发的异常。
(18)NotFiniteNumberException:当浮点值为正无穷大、负无穷大或非数字(NaN)时引发的异常。
(19)NotSupportedException:当调用的方法不受支持,或试图读取、查找或写入不支持调用功能的流时引发的异常。
(20)NullReferenceException:尝试取消引用空对象引用时引发的异常。
(21)OutOfMemoryException:没有足够的内存继续执行程序时引发的异常。
(22)OverflowException:算术运算、类型转换或转换操作导致溢出时引发的异常。
(23)RankException:将维数错误的数组传递给方法时引发的异常。
(24)StackOverflowException:因包含的嵌套方法调用过多而导致执行堆栈溢出时引发的异常。
(25)TypeInitializationException:作为由类初始值设定项引发的异常周围的包装引发的异常。
3.4.2 try-catch-finally语句
在C#语言中,try语句提供一种机制,用于捕捉在try块的执行期间发生的各种类型异常。使用try块来对可能受异常影响的代码进行分区,并使用catch块来处理所产生的任何异常。无论是否引发了异常,都可以使用finally块来执行代码。有时需要这样做,因为如果引发了异常,将不会执行try/catch构造后面的代码。
try块必须与catch或finally块一起使用,并且可以包括多个catch块。
try-catch-finally语句的语法格式如下:
try { // 可能发生异常的代码 } catch(class-type-1 identifier-1) { // 处理异常类class-type-1的代码 } catch(class-type-2 identifier-2) { // 处理异常类class-type-2的代码 } catch(class-type-n identifier-n) { // 处理异常类class-type-n的代码 } finally { // 清除异常的代码 }
其中class-type是一个可选项,必须为System.Exception及其派生的类型,或者以System.Exception(或其子类)作为其有效基类的参数类型。当catch子句同时指定class-type和identifier时,相当于声明了一个具有给定名称和类型的异常变量,此异常变量相当于一个范围覆盖整个catch块的局部变量。在catch块的执行期间,此异常变量表示当前正在处理的异常。
try块、catch块和finally块有以下3种组合形式。
(1)try-catch:在一个try块后面跟一个或多个catch块。
(2)try-finally:在一个try块后面跟一个finally块。
(3)try-catch-finally:在一个try块后面跟一个或多个catch块和一个finally块。
try-catch-finally语句的执行流程如下。
(1)控制转到try块,当控制到达try块的结束点时,如果该try语句具有finally块,则执行该块,然后控制转到try语句的结束点。
(2)如果在try块执行期间有一个异常传播到try语句,则按catch子句出现的顺序(如果有)逐个进行检查,以找到一个合适的异常处理程序。第1个指定了异常类型或该异常类型的基类型的catch子句被认为是一个匹配项。如果catch子句没有指定异常类,则可以处理任何异常,因为常规catch子句被认为是任何异常类型的匹配项。
(3)如果找到匹配的catch子句,而且该子句声明了一个异常变量,则异常对象被赋给这个异常变量,然后控制转到匹配的catch块。当控制到达catch块的结束点时,如果try语句具有一个finally块,则执行该块,然后把控制转到try语句的结束点。
(4)在try块执行期间,如果有一个异常传播到catch语句,而且该try语句具有finally块,则执行该块,然后该异常传播到更外面一层(封闭)的try语句;如果这个try语句没有catch子句或没有与异常匹配的catch子句,而且该try语句具有finally块,则执行finally块,然后这个异常传播到更外面一层(封闭)的try语句。
(5)finally块中的语句总是在控制离开try语句时被执行。无论是什么原因引起控制转移(正常执行到达结束点,执行了break、continue、goto或return语句,或将异常传播到try语句之外),情况都是如此。
【例3-12】创建一个C#控制台应用程序,说明如何使用try-catch-finally语句捕获和处理异常。在执行整数除法运算的过程中,处理3 类异常。即除数为0、输入了非数字字符串及数字超出整数范围,程序运行结果如图3-13所示。
图3-13 程序运行结果
【设计步骤】
(1)在解决方案chapter03中添加一个控制台应用程序项目,项目名称为“ConsoleApplication3-12”。保存在F:\Visual C sharp 2008\chapter03文件夹中,将该项目设置为启动项目。
(2)在代码编辑器中打开源文件Program.cs,在Main方法中输入以下代码:
int x, y, z = 0; Console.Title = "异常处理示例"; try { Console.Write("请输入一个整数(被除数):"); x = int.Parse(Console.ReadLine()); Console.Write("请输入另一个整数(除数):"); y = int.Parse(Console.ReadLine()); z = x / y; } catch (FormatException e) { Console.WriteLine("发生异常:{0}", e.GetType()); Console.WriteLine(e.Message); Console.WriteLine("应输入一个整数。"); } catch (DivideByZeroException e) { Console.WriteLine("发生异常:{0}", e.GetType()); Console.WriteLine(e.Message); Console.WriteLine("除数不能等于零。"); } catch (OverflowException e) { Console.WriteLine("发生异常:{0}", e.GetType()); Console.WriteLine(e.Message); Console.WriteLine("输入的数字已超出整数范围。"); } finally { Console.WriteLine("商 = {0}", z); Console.WriteLine("谢谢使用!\n"); }
(3)按Ctrl+F5组合键编译并运行应用程序,分成以下4种情况测试程序。
● 被除数和除数都是非零整数,此时未引发异常。
● 被除数为非零整数,除数为0。此时引发DivideByZeroException异常,商为0并显示“谢谢使用!”信息。
● 被除数或除数超出int类型的取值范围,此时引发OverflowException异常,商为0并显示“谢谢使用!”信息。
● 输入被除数或除数时输入了非数字字符(如字母kkkk),此时引发FormatException异常,商为0并显示“谢谢使用!”信息。
3.4.3 throw语句
如前所述,在执行C#语句和表达式的过程中有时会出现一些例外情况,使某些操作无法正常完成,此时就会引发一个异常。除此之外,还可以使用throw语句立即无条件地引发异常,其语法格式如下。
throw expression;
其中表达式expression是可选项,必须表示一个异常对象,可以是类类型System.Exception的值及其派生的类类型的值,或者以System.Exception(或其子类)作为其有效基类的类型参数类型的值。如果表达式的值为null,则引发System.NullReferenceException。
带表达式的throw语句引发一个异常,此异常的值就是通过计算该表达式产生的值;不带表达式的throw语句只能用在catch块中,在这种情况下,这个语句重新引发当前正由该catch块处理的异常。由于throw语句无条件地将控制转到他处,因此永远无法到达其结束点,即控制永远不会到达紧跟在throw后面的语句。
当引发一个异常时,控制转到封闭其try语句中能够处理该异常的第1个catch子句。从引发一个异常开始直至将控制转到该异常的一个合适的异常处理程序止,这个过程称为“异常传播”。异常传播过程由重复地执行如下步骤,直至找到一个与该异常匹配的catch子句。
(1)在当前函数成员中检查每个封闭着引发点的try语句。对于每个语句S,从最里层的try语句开始逐次向外。直到最外层的try语句结束,按如下步骤计算。
● 如果S的try块封闭引发点(指引发该异常的位置),并且S具有一个或多个catch子句,则按其出现的顺序检查这些catch子句,以找到合适的异常处理程序。第1个指定了异常类型或该异常类型的基类型的catch子句被认为是一个匹配项,常规catch子句被认为是任何异常类型的匹配项。如果找到匹配的catch子句,则通过将控制转到该catch子句的块来完成异常传播。
● 如果找不到匹配的catch子句,并且S的try块或catch块封闭引发点,S具有finally块,则控制将转到finally块。如果在该finally块内引发另一个异常,则终止当前异常的处理;否则当控制到达finally块的结束点时将继续处理当前异常。
(2)如果在当前函数成员调用中没有找到异常处理程序,则终止对该函数成员的调用。然后为该函数成员的调用方重复执行上面的步骤,并使用对应于该调用函数成员的语句的引发点。
(3)如果上述异常处理终止了当前线程中的所有函数成员调用,则表明此线程没有该异常的处理程序,那么线程本身将终止。此类终止会产生什么影响,应由实现来定义。
在编程实践中,可以使用throw语句以程序方式显式引发系统预定义异常或用户自定义的异常。也可以使用throw语句再次引发捕获的异常,比较好的编码做法是为再次引发的异常添加信息以在调试时提供更多信息。
例如,使用throw语句引发参数为空的系统预定义异常ArgumentNullException:
throw new ArgumentNullException();
在下面的例子中,首先声明一个自定义异常类MyException,然后使用throw语句引发这个异常:
class MyException : System.Exception{} . . . . . . throw new MyException();
【例3-13】创建一个C#控制台应用程序,说明如何使用try-catch-finally语句捕获和处理异常,程序运行结果如图3-14所示。
图3-14 程序运行结果
【设计步骤】
(1)在解决方案chapter03中添加一个控制台应用程序项目,项目名称为“ConsoleApplication3-13”。保存在F:\Visual C sharp 2008\chapter03文件夹中,将该项目设置为启动项目。
(2)在代码编辑器中打开源文件Program.cs,在Main方法中输入以下代码:
Console.Title = "throw语句应用示例"; string username = ""; try { Console.Write("请输入用户名:"); username = Console.ReadLine(); if (username == "") { throw new ArgumentNullException("", "用户名不能为空!\n"); } } catch (ArgumentNullException e) { Console.ForegroundColor = ConsoleColor.Red; Console.WriteLine(e.Message); } finally { Console.ForegroundColor = ConsoleColor.Black; if (username != "") { Console.WriteLine("{0}用户,你好!\n", username); } }
(3)按Ctrl+F5组合键编译并运行应用程序,并分成以下两种情况测试程序。
● 看到提示信息后按Enter键,引发异常。
● 看到提示信息后输入用户名并按Enter键,显示欢迎信息。
3.5 程序调试
在使用Visual C#开发应用的过程中,经常要查找并修改程序中出现的错误,这个过程称为“程序调试”。本节首先介绍程序错误的类型,然后讨论如何调试程序。
3.5.1 程序错误类型
利用C#进行程序设计时,通常会出现以下3种类型的错误。
(1)编译错误:这类错误又称为“语法错误”,一般是由于没有按照正确的语法规则编写语句而产生的。例如,拼错关键字,或者漏掉语句结尾的分号等。对于编译错误,系统会自动检查并显示在错误列表中。
(2)运行时错误:这类错误又称为“实时错误”,是程序运行期间发生的错误,通常是一个语句试图执行一个不能执行的操作。例如,在整数除法中除数为0,或者打开文件时指定了一个不存在的文件等。在编译过程中无法发现这类错误,只有在运行期间才会暴露出来。如果不处理这类错误,程序有可能被意外终止,甚至会导致停止响应。在C#语言中,可以通过异常处理语句来捕获和处理运行时错误。
(3)逻辑错误:这类错误的特点是程序可以通过编译,并正常运行,但程序运行时未按预期方式运行或未得到预期结果。当发生逻辑错误时,需要开发人员不断跟踪调试程序,分析调试结果,才能找到产生错误的原因。逻辑错误通常需要借助于调试工具来检查。
3.5.2 程序调试方法
Visual Studio 2008提供了一个功能强大的调试器,可以用来设置断点、分析程序的运行过程,并且观察变量和表达式的值变化情况,从而帮助开发人员快速发现错误的来源。
本节首先介绍如何通过设置断点来进入调试过程,然后讨论如何使用局部变量窗口、监视窗口和快速监视窗口等调试工具来跟踪程序的运行过程。
1. 设置断点
断点是调试程序时经常使用的工具,使用调试器时需要在代码中插入断点,以通知调试器在某个位置中断应用程序并暂停执行。当发生中断时,称程序和调试器处于中断模式。
若要在代码中设置或清除断点,可在代码编辑器中执行下列操作之一。
(1)单击要插入或清除断点的代码行,然后单击“调试”→“切换断点”命令。
(2)单击要插入或清除断点的代码行,按F9功能键。
(3)单击要插入或清除断点代码行前的指示器边距内部,设置断点后将会出现断点图标,如图3-15所示。
图3-15 在代码中设置断点
2. 启动调试
设置断点后,即可调试程序。对于包含多个项目的解决方案,首先需要在解决方案资源管理器中单击要调试的项目,然后单击“项目”→“设置为启动项目”命令,以便把该项目设置为启动项目。若要对启动项目启动调试,单击“调试”→“启动调试”命令或按F5功能键,或单击标准工具栏中的“启动调试”按钮,如图3-16所示。
图3-16 “启动调试”按钮
启动调试后,当程序遇到第1个断点时会自动暂停。原来的断点图标变成,而该代码行呈现为黄色背景高亮显示,如图3-17所示。
图3-17 程序运行到断点处暂停
在中断模式下,可以使用调试工具栏上的按钮来执行相应的调试功能,如图3-18所示。
图3-18 调试工具栏
A-继续(F5)B-全部中断C-停止调试D-重新启动E-显示下一句F-逐语句(F11)G-逐过程(F10)H-跳出
3. 使用局部变量窗口
局部变量窗口用于列出当前作用域(当前正在执行的方法成员)内的变量值和类型,使用该窗口可以跟踪这些变量的变化情况,如图3-19所示。
图3-19 局部变量窗口
4. 使用监视窗口
监视窗口用于计算和显示变量和表达式的值和类型,使用该窗口可以跟踪这些变量和表达式的值的变化情况,如图3-20所示。
图3-20 监视窗口
与局部变量窗口不同,使用监视窗口时需要把要监视的变量或表达式添加到该窗口中。为此在代码编辑器中右击要监视的变量或表达式,然后单击快捷菜单中的“添加监视”命令或在监视窗口的“名称”列中直接输入要监视的变量名或表达式。
5. 使用快速监视窗口
快速监视窗口可以用于计算一个变量或表达式的值,但不能在程序运行过程中跟踪变量或表达式值的变化,如图3-21所示。
图3-21 快速监视窗口
为打开快速监视窗口,在代码编辑器窗口右击要计算的变量或表达式,然后单击快捷菜单中的“快速监视”命令。
3.6 预处理器指令
C#提供了一些预处理器指令,主要功能是完成定义符号、条件编译并报告编译和警告信息。这些预处理器指令都以数字符“#”开头,并且必须是一个代码行中的唯一指令。
3.6.1 定义符号
使用#define预处理器指令可以定义一个符号,当将符号用做传递给#if指令的表达式时,此表达式的计算结果为true。例如:
#define DEBUG
用#define定义的符号与具有同一名称的变量不冲突,不应将变量名传递到预处理器指令中,并且只能用预处理器指令计算符号。用#define创建的符号的范围是在其中定义该符号的文件。
符号可以用于指定编译的条件,在程序中可以用#define指令定义符号,也可以使用#if或#elif指令来测试符号。还可以用#undef指令来取消定义符号,但是不能为符号赋值。
可以用#undef指令来取消已经定义的符号,如果符号未定义,则#undef指令不起任何作用。
#define指令和#undef指令必须放在源文件的开头,即所有其他非注释语句和非预处理器指令的前面。
3.6.2 条件编译
条件编译指在编译过程中根据条件编译预处理器指令来包括或排除部分程序代码段,C#提供了以下4个条件编译指令。
(1)#if指令:用于开始条件指令,测试一个或多个符号以查看其是否计算为true。如果为true,则编译器将计算位于#if与最近的#endif指令之间的所有代码。
(2)#else指令:用于创建复合条件指令,如果前面的#if或(可选)#elif指令中的任何表达式都不为true,则编译器将计算#else与后面的#endif之间的所有代码。#else之后的下一条预处理器指令必须是#endif。
(3)#elif指令:用于创建复合条件指令,如果前面的#if和任何#elif(可选)指令表达式的计算结果都不是true,则将计算#elif表达式;否则编译器将计算位于#elif和下一个条件指令之间的所有代码。
(4)#endif指令:指定以#if指令开头的条件指令的结尾,以#if指令开始的条件指令必须用#endif指令显式终止。
上述条件编译指令有以下4种基本组合形式。
(1)#if (条件)…#endif。
(2)#if (条件)…#else…#endif。
(3)#if (条件)…#elif (条件)…#endif。
(4)#if (条件) …#elif (条件)…#else…#endif。
【例3-14】创建一个C#控制台应用程序,说明如何使用符号定义指令和条件编译指令,程序运行结果如图3-22所示。
图3-22 程序运行结果
【设计步骤】
(1)在解决方案chapter03中添加一个控制台应用程序项目,项目名称为“ConsoleApplication3-14”。保存在F:\Visual C sharp 2008\chapter03文件夹中,将该项目设置为启动项目。
(2)在代码编辑器中打开源文件Program.cs,在文件顶部输入以下预处理器指令:
#define AA #define BB #define CC #undef CC
(3)在Main方法中输入以下代码:
Console.Title = "条件编译示例"; #if AA && BB || !CC Console.WriteLine("满足预编译条件!\n"); #else Console.WriteLine("不满足预编译条件!\n"); #endif
(4)按Ctrl+F5组合键编译并运行应用程序。
3.6.3 报告编译错误和警告
#error指令用于在代码中的特定位置生成错误,#warning用于在代码的特定位置生成一级警告,其语法格式如下。
#error字符串 #warning字符串
上述两个指令通常用在条件编译指令中,用于控制编译器编译时输出。#error指令以编译错误形式输出字符串,程序无法被成功编译;#warning指令则以编译警告形式输出字符串,程序仍能成功编译。
【例3-15】创建一个C#控制台应用程序,说明如何在代码的特定位置生成编译错误和警告,运行程序时错误列表显示的信息3-22所示。
图3-22 错误列表显示的信息
【设计步骤】
(1)在解决方案chapter03中添加一个控制台应用程序项目,项目名称为“ConsoleApplication3-15”。保存在F:\Visual C sharp 2008\chapter03文件夹中,将该项目设置为启动项目。
(2)在代码编辑器中打开源文件Program.cs,在文件顶部输入以下预处理器指令:
#define ERROR #define WARNING
(3)在Main方法中输入以下代码:
#if (ERROR && WARNING) #error出现编译错误 #warning出现编译警告 #endif Console.WriteLine("Welcome to Visual C#!");
(4)按Ctrl+F5组合键编译程序,出现编译错误和警告,程序无法运行。
3.6.4 定义代码块
使用#region指令可以在使用Visual Studio代码编辑器的大纲显示功能时指定可展开或折叠的代码块,#region块必须以#endregion指令终止。语法格式如下:
#region字符串 代码块 #endregion
应当说明的是,#region块不能与#if块重叠。但是可以将#region块嵌套在#if块内,或将#if块嵌套在#region块内。
【例3-16】创建一个C#控制台应用程序,说明如何使用#region指令定义可折叠的代码块,折叠前后的代码块分别如图3-24和图3-25所示。
图3-24 折叠前的代码块
图3-25 折叠后代码块
【设计步骤】
(1)在解决方案chapter03中添加一个控制台应用程序项目,项目名称为“ConsoleApplication3-16”。保存在F:\Visual C sharp 2008\chapter03文件夹中,将该项目设置为启动项目。
(2)在代码编辑器中打开源文件Program.cs,在Main方法上方定义一个代码块:
#region定义静态方法 static int Add(int x, int y) { int z; z = x + y; return z; } #endregion
(3)在Main方法中输入以下代码:
#region声明局部变量 int a; int b; int c; #endregion a = 3; b = 6; Console.Title = "定义代码块指令应用示例"; c = Add(a, b); Console.WriteLine("{0} + {1} = {2}", a, b, c);
(4)在代码编辑器窗口中折叠并展开定义的两个代码块。