4.8 控制流程语句
在更详细地探讨了布尔表达式之后,我们就可以更清楚地描述C#支持的控制流程语句。有经验的程序员已熟悉了其中许多语句,所以可快速浏览本节内容,找出C#特有的信息。特别是foreach循环,它对许多程序员来说是新的。
4.8.1 while和do/while循环
目前学习的都是只执行一遍的程序。但计算机的关键优势之一是能多次执行相同操作。为此需要创建指令循环。本节讨论的第一个指令循环是while循环,它是最简单的条件循环。while语句的常规形式如下:
条件(condition)必须是布尔表达式,只要它求值为true,作为循环主体的语句(statement)就会反复执行。如果条件求值为false,就跳过循环主体,从它之后的语句执行。注意循环主体会一直执行,即使这个过程中条件变成false。除非回到“循环顶部”重新求值条件,而且结果是false,否则循环不会退出。代码清单4.43用一个斐波那契计算器演示了while循环语句的用法。
代码清单4.43 while循环示例
斐波那契数是斐波那契数列的成员,数列中所有数都是数列中前两个数之和。数列最开头两个数是1和1。代码清单4.43中提示输入整数,使用while循环寻找比输入的数大的第一个斐波那契数。
初学者主题:何时使用while循环
本章剩余部分会讲到其他使代码块反复执行的循环结构。术语循环主体指的是while结构中执行的语句(通常是代码块)。这是因为在达成退出条件之前,代码会一直“循环”。需要明白在什么时候选择什么循环结构。如条件为true就一直执行某个操作,就选择while结构。for主要用于重复次数已知的循环,比如从0~n的计数。do/while类似于while循环,区别在于循环主体至少执行一次。
do/while循环与while循环非常相似,只是最适合需要循环1~n次的情况,而且n在循环开始前无法确定。这个模式经常用于提示用户输入。代码清单4.44是从井字棋程序中提取出来的。
代码清单4.44 do/while循环示例
代码清单4.44在每次迭代[1]或循环开始的时候将valid设为false。接着提示并获取用户输入的数。虽然这部分在代码中省略了,但接下来的操作是检查输入是否正确。如正确,就将true赋给valid。由于代码使用do/while而不是while语句,所以至少提示用户输入一次。
do/while循环的常规形式如下:
和所有控制流程语句一样,循环主体通常是代码块,以便执行多个语句。但也可将单一语句作为循环主体(标签语句和局部变量声明除外)。
4.8.2 for循环
for循环反复执行代码块直至满足指定条件。这一点与while循环非常相似。区别在于,for循环有一套内建的语法规定了如何初始化、递增以及测试一个计数器的值。该计数器称为循环变量。由于循环语法中专门有一个位置是为递增/递减操作保留的,所以递增/递减操作符经常作为for循环的一部分使用。
代码清单4.45展示了如何使用for循环显示整数的二进制形式。输出4.21展示了结果。
代码清单4.45 使用for循环
输出4.21
代码清单4.45执行位掩码64次,对用户输入的数中的每一位都应用一次。for循环头部包含三个部分。第一部分声明并初始化变量count,第二部分描述for循环主体的执行条件,第三部分描述如何更新循环变量。for循环的常规形式如下:
下面解释了for循环的各个部分。
·initial(初始化)执行首次迭代前的初始化操作。在代码清单4.45中,它声明并初始化count变量。initial表达式不一定非要声明新变量。例如,可事先声明好变量,在for循环中只将其初始化。也可完全省略该部分。如在这里声明变量,其作用域仅限于for语句头部和主体。
·condition(条件)指定循环结束条件。条件为false终止循环,这和while循环一样。只有条件求值为true才会执行for循环主体。本例在count大于或等于64时退出循环。
·loop(循环)表达式在每次迭代后求值。本例的循环表达式count++会在mask右移位(mask >>=1)之后,对条件求值之前执行。第64次迭代时count递增到64,造成条件变成false,因而终止循环。
·statement(语句)是在条件表达式为true时执行的“循环主体”代码。
代码清单4.45的for循环的执行步骤可用以下伪代码表示:
1.声明count并将其初始化为0。
2.如count小于64,转到步骤3;否则转到步骤7。
3.计算bit并显示它。
4.对mask执行右移位。
5.count递增1。
6.跳回步骤2。
7.继续执行循环主体之后的语句。
for语句头部三部分均可省略。for(;;){...}完全有效,只要有办法从循环中退出以避免无限循环(缺失的条件默认为常量true)。
initial和loop表达式支持多个循环变量,如代码清单4.46所示。
代码清单4.46 使用多个表达式的for循环
结果如输出4.22所示。
输出4.22
initial部分声明并初始化两个循环变量。尽管看起来复杂,但起码像是在一个语句中声明多个局部变量,还算正常。loop部分则看起来不正常,因为它包含以逗号分隔的表达式列表,而非单一表达式。
设计规范
·如果被迫要写包含复杂条件和多个循环变量的for循环,考虑重构方法使控制流程更容易理解。
任何for循环都能改写成while循环:
设计规范
·事先知道循环次数,且循环中要用到控制循环次数的“计数器”时,要使用for循环。
·事先不知道循环次数而且不需要计数器时,要使用while循环。
4.8.3 foreach循环
C#最后一个循环语句是foreach,它迭代数据项集合,设置循环变量来依次表示其中每一项。循环主体可对数据项执行指定操作。foreach循环的特点是每一项只迭代一次:不会像其他循环那样出现计数错误,也不可能越过集合边界。
foreach语句的常规形式如下:
下面解释了foreach语句的各个部分。
·type为代表collection中每一项的variable声明数据类型。可将类型设为var,编译器将根据集合类型推断数据项类型。
·variable是只读变量,foreach循环自动将collection中的下一项赋给它。variable的作用域限于循环主体。
·collection是代表多个数据项的表达式,比如数组。
·statement是每次迭代都要执行的循环主体。
来看代码清单4.47展示的一个简单foreach循环。
代码清单4.47 使用foreach循环判断剩余走棋
输出4.23展示了代码清单4.47的结果。
输出4.23
执行到foreach语句时,将cells数组的第一项,也就是值'1'赋给cell变量。然后执行foreach循环主体。if语句判断cell的值是否等于'O'或'X',两者都不是就在控制台上输出cell的值。下次循环将数组的下个值赋给cell,以此类推。
必须记住,foreach循环期间禁止修改循环变量(这里是cell)。另外,循环变量从C# 5.0开始的行为稍微有别于之前的版本。在循环主体中通过Lambda表达式或匿名方法使用循环变量需注意该差别。详情参见第13章。
初学者主题:何时使用switch语句
有时需要在连续几个if语句中比较同一个值,如代码清单4.48的input变量所示。
代码清单4.48 用if语句检查玩家输入
代码验证用户输入的文本,确定是一步有效的井字棋走棋。例如,假定input的值是9,那么程序不得不执行9次求值。显然,更好的思路是只在一次求值之后就跳转到正确的代码。这种情况下应使用switch语句。
4.8.4 基本switch语句
当将一个值和多个常量值比较时,switch比if语句更易理解。其常规形式如下:
下面解释了switch语句的各个部分。
·expression是要和不同常量比较的值。该表达式的类型决定了switch的“主导类型”。允许的主导类型包括bool、sbyte、byte、short、ushort、int、uint、long、ulong、char、任何枚举(enum)类型(详情参见第9章)以及上述所有值类型的可空类型以及string。
·constant是和主导类型兼容的任何常量表达式。
·一个或多个case标签(或default标签),后跟一个或多个语句(称为一个switch小节)。上例只显示了两个switch小节。代码清单4.49的switch语句包含三个。
·statements是在expression的值等于某个标签指定的constant值时执行的一个或多个语句。这组语句的结束点必须“不可到达”[2]。换言之,不能“直通”或“贯穿”到下个switch小节。所以,最后一个语句通常是跳转语句,比如break、return或goto。
设计规范
·不要使用continue作为跳转语句退出switch小节。尽管switch在循环中时这样写合法,但很容易对之后的switch小节中出现的break产生困惑。
switch语句应至少有一个switch小节,switch(x){}合法但会产生一个警告。另外,虽然一般情况下应避免省略大括号,但一个例外是应省略case和break语句的大括号,因为这两个关键字本身就指示了块的开始和结束。
代码清单4.49的switch语句在语义上等价于代码清单4.48的一系列if语句。
代码清单4.49 将if语句替换成switch语句
代码清单4.49中的input是要测试的表达式。由于input是字符串,所以主导类型是string。如input的值是"1","2",…,"9",那么走棋有效(valid=true),然后更改相应的单元格,使之与当前用户的标记(X或O)匹配。遇到break语句会立即跳转到switch语句之后的语句。
下一个switch小节描述如何处理空字符串""或"quit"。若input等于这两个值之一,就将valid设为true。没有和测试表达式匹配的其他case标签,就执行switch的default小节。
语言对比:C++—— switch语句贯穿
在C++中,如switch小节不以跳转语句结尾,控制会“贯穿”(直通)至下个switch小节并执行其中的代码。由于在C++中容易出错,所以C#不允许控制从一个switch小节自然贯穿到下一个。C#的设计者认为这样可以更好地防止bug并增强代码的可读性。如希望switch小节执行另一个switch小节中的代码,要显式使用goto语句来实现,详情参见4.9.3节。
switch语句有几点要注意:
·无任何小节的switch语句会产生编译器警告,但语句仍能通过编译。
·各小节可为任意顺序,default小节不一定要出现在switch语句最后,甚至可以省略。
·C#要求每个switch小节(包括最后一个小节)的结束点“不可到达”。这意味着switch小节通常以break、return、throw或goto结尾。
C# 7.0为switch语句引入了模式匹配,switch表达式可使用任何数据类型,而非只能使用前面描述的有限几个。这样switch语句就可基于switch表达式的类型使用(可在case标签中声明变量)。最后,模式匹配switch语句支持条件表达式,所以不仅可以用类型来标识应执行的case标签,还可以在case标签末尾使用布尔表达式标识该标签的执行条件。第7章更多地讨论了模式匹配switch语句。
[1] 每一次循环都称为一次“迭代”。——译者注
[2] C#语言规范对结束点和可到达性的解释是这样的:“每个语句都有一个结束点(end point)。直观地讲,语句的结束点是紧跟在语句后面的那个位置。复合语句(包含嵌入语句的语句)的执行规则规定了当控制到达一个嵌入语句的结束点时所采取的操作。例如,当控制到达块中某个语句的结束点时,控制就转到该块中的下一个语句。如果执行流程可能到达某个语句,则称该语句可到达(reachable)。相反,如果某个语句不可能被执行,则称该语句不可到达(unreachable)。”——译者注