2.5 程序流控制
本节将介绍C#语言最基本的重要语句:控制程序流的语句。它们不是按代码在程序中的排列位置顺序执行的。
2.5.1 条件语句
条件语句可以根据条件是否满足或根据表达式的值来控制代码的执行分支。C#有两个控制代码的分支的结构:if语句,测试特定条件是否满足;switch语句,比较表达式和多个不同的值。
1. if语句
对于条件分支,C#继承了C和C++的if...else结构。对于用过程语言编程的人,其语法非常直观:
if (condition) statement(s) else statement(s)
如果在条件中要执行多个语句,就需要用花括号({ ... })把这些语句组合为一个块(这也适用于其他可以把语句组合为一个块的C#结构,如for和while循环)。
bool isZero; if (i == 0) { isZero = true; WriteLine("i is Zero"); } else { isZero = false; WriteLine("i is Non-zero"); }
还可以单独使用if语句,不加最后的else语句。也可以合并else if子句,测试多个条件(代码文件IfStatement/Program.cs)。
using static System.Console; namespace Wrox { class Program { static void Main() { WriteLine("Type in a string"); string input; input = ReadLine(); if (input == "") { WriteLine("You typed in an empty string."); } else if (input.Length < 5) { WriteLine("The string had less than 5 characters."); } else if (input.Length < 10) { WriteLine("The string had at least 5 but less than 10 Characters."); } WriteLine("The string was " + input); } }
添加到if子句中的else if语句的个数不受限制。
注意,在上面的例子中,声明了一个字符串变量input,让用户在命令行输入文本,把文本填充到input中,然后测试该字符串变量的长度。代码还显示了在C#中如何进行字符串处理。例如,要确定input的长度,可以使用input.Length。
对于if,要注意的一点是如果条件分支中只有一条语句,就无须使用花括号:
if (i == 0) WriteLine("i is Zero"); // This will only execute if i == 0 WriteLine("i can be anything"); // Will execute whatever the // value of i
但是,为了保持一致,许多程序员只要使用if语句,就加上花括号。
提示:在if语句中不使用花括号,可能在维护代码时导致错误。无论if语句返回true还是false,都常常给if语句添加第二个语句。每次都使用花括号,就可以避免这个编码错误。
使用if语句的一个指导原则是只有语句和if语句写在同一行上,才不允许程序员使用花括号。遵守这条指导原则,程序员就不太可能在添加第二个语句时不添加花括号。
前面介绍的if语句还演示了用于比较数值的一些C#运算符。特别注意,C#使用“= =”对变量进行等于比较。此时不要使用“=”,一个“=”用于赋值。
在C#中,if子句中的表达式必须等于布尔值(Boolean)。不能直接测试整数(如从函数中返回的值),而必须明确地把返回的整数转换为布尔值true或false,例如,将值与0或null进行比较:
if (DoSomething() ! = 0) { // Non-zero value returned } else { // Returned zero }
2. switch语句
switch…case语句适合于从一组互斥的可执行分支中选择一个执行分支。其形式是switch参数的后面跟一组case子句。如果switch参数中表达式的值等于某个case子句旁边的某个值,就执行该case子句中的代码。此时不需要使用花括号把语句组合到块中;只需要使用break语句标记每段case代码的结尾即可。也可以在switch语句中包含一条default子句,如果表达式不等于任何case子句的值,就执行default子句的代码。下面的switch语句测试integerA变量的值:
switch (integerA) { case 1: WriteLine("integerA = 1"); break; case 2: WriteLine("integerA = 2"); break; case 3: WriteLine("integerA = 3"); break; default: WriteLine("integerA is not 1, 2, or 3"); break; }
注意case值必须是常量表达式;不允许使用变量。
C和C++程序员应很熟悉switch…case语句,而C#的switch…case语句更安全。特别是它禁止几乎所有case中的失败条件。如果激活了块中靠前的一条case子句,后面的case子句就不会被激活,除非使用goto语句特别标记也要激活后面的case子句。编译器会把没有break语句的case子句标记为错误,从而强制实现这一约束:
Control cannot fall through from one case label ('case 2:') to another
在有限的几种情况下,这种失败是允许的,但在大多数情况下,我们不希望出现这种失败,而且这会导致出现很难察觉的逻辑错误。让代码正常工作,而不是出现异常,这样不是更好吗?
但在使用goto语句时,会在switch…cases中重复出现失败。如果确实想这么做,就应重新考虑设计方案了。
下面的代码说明了如何使用goto模拟失败,得到的代码会非常混乱:
// assume country and language are of type string switch(country) { case "America": CallAmericanOnlyMethod(); goto case "Britain"; case "France": language = "French"; break; case "Britain": language = "English"; break; }
但有一种例外情况。如果一条case子句为空,就可以从这条case子句跳到下一条case子句,这样就可以用相同的方式处理两条或多条case子句了(不需要goto语句)。
switch(country) { case "au": case "uk": case "us": language = "English"; break; case "at": case "de": language = "German"; break; }
在C#中,switch语句的一个有趣的地方是case子句的顺序是无关紧要的,甚至可以把default子句放在最前面!因此,任何两条case都不能相同。这包括值相同的不同常量,所以不能这样编写:
// assume country is of type string const string england = "uk"; const string britain = "uk"; switch(country) { case england: case britain: // This will cause a compilation error. language = "English"; break; }
上面的代码还说明了C#中的switch语句与C++中的switch语句的另一个不同之处:在C#中,可以把字符串用作测试的变量。
2.5.2 循环
C#提供了4种不同的循环机制(for、while、do...while和foreach),在满足某个条件之前,可以重复执行代码块。
1. for循环
C#的for循环提供的迭代循环机制是在执行下一次迭代前,测试是否满足某个条件,其语法如下:
for (initializer; condition; iterator): statement(s)
其中:
● initializer是指在执行第一次循环前要计算的表达式(通常把一个局部变量初始化为循环计数器)。
● condition是在每次循环的新迭代之前要测试的表达式(它必须等于true,才能执行下一次迭代)。
● iterator是每次迭代完要计算的表达式(通常是递增循环计数器)。
当condition等于false时,迭代停止。
for循环是所谓的预测试循环,因为循环条件是在执行循环语句前计算的,如果循环条件为假,循环语句就根本不会执行。
for循环非常适合用于一个语句或语句块重复执行预定的次数。下面的例子就是for循环的典型用法,这段代码输出0~99的整数:
for (int i = 0; i < 100; i = i + 1) { WriteLine(i); }
这里声明了一个int类型的变量i,并把它初始化为0,用作循环计数器。接着测试它是否小于100。因为这个条件等于true,所以执行循环中的代码,显示值0。然后给该计数器加1,再次执行该过程。当i等于100时,循环停止。
实际上,上述编写循环的方式并不常用。C#在给变量加1时有一种简化方式,即不使用i = i+1,而简写为i++:
for (int i = 0; i < 100; i++) { // etc. }
也可以在上面的例子中给循环变量i使用类型推断。使用类型推断时,循环结构变成:
for (var i = 0; i < 100; i++) { // etc. }
嵌套的for循环非常常见,在每次迭代外部循环时,内部循环都要彻底执行完毕。这种模式通常用于在矩形多维数组中遍历每个元素。最外部的循环遍历每一行,内部的循环遍历某行上的每个列。下面的代码显示数字行,它还使用另一个Console方法Console.Write(),该方法的作用与Console.WriteLine()相同,但不在输出中添加回车换行符(代码文件ForLoop/Program.cs ):
using static System.Console; namespace Wrox { class Program { static void Main() { // This loop iterates through rows for (int i = 0; i < 100; i+=10) { // This loop iterates through columns for (int j = i; j < i + 10; j++) { Write($" {j}"); } WriteLine(); } } } }
尽管j是一个整数,但它会自动转换为字符串,以便进行连接。
上述例子的结果是:
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99
尽管在技术上,可以在for循环的测试条件中计算其他变量,而不计算计数器变量,但这不太常见。也可以在for循环中忽略一个表达式(甚至所有表达式)。但此时,要考虑使用while循环。
2. while循环
与for循环一样,while也是一个预测试循环。其语法是类似的,但while循环只有一个表达式:
while(condition) statement(s);
与for循环不同的是,while循环最常用于以下情况:在循环开始前,不知道重复执行一个语句或语句块的次数。通常,在某次迭代中,while循环体中的语句把布尔标志设置为false,结束循环,如下面的例子所示:
bool condition = false; while (! condition) { // This loop spins until the condition is true. DoSomeWork(); condition = CheckCondition(); // assume CheckCondition() returns a bool }
3. do…while循环
do...while循环是while循环的后测试版本。这意味着该循环的测试条件要在执行完循环体之后评估。因此do...while循环适用于循环体至少执行一次的情况:
bool condition; do { // This loop will at least execute once, even if Condition is false. MustBeCalledAtLeastOnce(); condition = CheckCondition(); } while (condition);
4. foreach循环
foreach循环可以迭代集合中的每一项。现在不必考虑集合的准确概念(第11章将详细介绍集合),只需要知道集合是一种包含一系列对象的对象即可。从技术上看,要使用集合对象,就必须支持IEnumerable接口。集合的例子有C#数组、System.Collection名称空间中的集合类,以及用户定义的集合类。从下面的代码中可以了解foreach循环的语法,其中假定arrayOfInts是一个int类型数组:
foreach (int temp in arrayOfInts) { WriteLine(temp); }
其中,foreach循环每次迭代数组中的一个元素。它把每个元素的值放在int类型的变量temp中,然后执行一次循环迭代。
这里也可以使用类型推断。此时,foreach循环变成:
foreach (var temp in arrayOfInts) { // etc. }
temp的类型推断为int,因为这是集合项的类型。
注意,foreach循环不能改变集合中各项(上面的temp)的值,所以下面的代码不会编译:
foreach (int temp in arrayOfInts) { temp++; WriteLine(temp); }
如果需要迭代集合中的各项,并改变它们的值,应使用for循环。
2.5.3 跳转语句
C#提供了许多可以立即跳转到程序中另一行代码的语句,在此,先介绍goto语句。
1. goto语句
goto语句可以直接跳转到程序中用标签指定的另一行(标签是一个标识符,后跟一个冒号):
goto Label1; WriteLine("This won't be executed"); Label1: WriteLine("Continuing execution from here");
goto语句有两个限制。不能跳转到像for循环这样的代码块中,也不能跳出类的范围;不能退出try...catch块后面的finally块(第14章将介绍如何用try...catch...finally块处理异常)。
goto语句的名声不太好,在大多数情况下不允许使用它。一般情况下,使用它肯定不是面向对象编程的好方式。
2. break语句
前面简要提到过break语句——在switch语句中使用它退出某个case语句。实际上,break语句也可以用于退出for、foreach、while或do...while循环,该语句会使控制流执行循环后面的语句。
如果该语句放在嵌套的循环中,就执行最内部循环后面的语句。如果break放在switch语句或循环外部,就会产生编译错误。
3. continue语句
continue语句类似于break语句,也必须在for、foreach、while或do...while循环中使用。但它只退出循环的当前迭代,开始执行循环的下一次迭代,而不是退出循环。
4. return语句
return语句用于退出类的方法,把控制权返回方法的调用者。如果方法有返回类型,return语句必须返回这个类型的值;如果方法返回void,应使用没有表达式的return语句。