第七章 - 控制结构
本章重点
◆ 建立和组合逻辑测试
◆ 使用if和switch进行分支处理
◆ 使用while和for
◆ 使用exit和die退出页面的执行
如果不能让程序可因不同的情况来决定不同的执行,就很不容易编写出有用的程序。简单地说,输出显示变数的程序码行为取决于某个变数的值,做为一个程序设计师,我们可以透过不同的动作让程序对事件做出不同的回应(可依照外在世界、时间、使用者的输入或资料库的内容等等来配合)。
这种程序回应需要一种「控制结构(control structure)」,这个结构可控制指示在不同的情况下应该配合不同的程序码来执行。在上一章中,我们使用了if这样的控制结构,但没有真正深入讲解它,在这一章中,我们会介绍PHP提供的每种控制结构,并详细研究它们的运作与操作方式。
针对有经验的C语言程序设计师:在PHP的所有功能里,「控制」这部份是与C语言风格上最相似的,C语言中原来使用的所有结构都可以在这里使用,而且运作的方式也相同。如果你是有经验的C语言程序师可以跳过前面直接阅读本章末尾的小节。
我们将讨论的两大控制结构类型是分支(branch)和回圈(loop)。分支是程式执行通路上的一个分叉口,取决于某种测诗,程式可选择向左进行或向右进行,以后的路可能不相同,也可能重新汇合在一起。回圈是某种分支类型之一,它有一条执行路径转回到分支的开始处,可重覆进行测试度可能重覆循环执行。
在有效利用控制结构之前,必须能够有效地建构测试条件。我们先从最简单的测试开始,先了解常数TRUE和FALSE,然后在更复杂的程式码中使用这些测试。
Boolean运算式
本章中介绍的每种控制结构都含有两个截然不同的部份:一个是测试部份(决定往哪能个方向进行),一个是由测试的程式码(为单独的分支或是回圈)测试是透过Boolean运算求值进行的,以「真」或「非真」的判断为运算式的结果。
Boolean常数
最简单的运算式类型就是个简单值(simple value),最简单的Boolean值就是TRUE和FALSE常数,反之亦然。例如,我们可以在if-else叙述的测试部份嵌入它们:
if (TRUE)
print(“This will always print”);
else
print(“This will never print”);
上面的范例与下面的确叙述的是相同的:
if(FALSE)
print(“This will never print”);
else
print(“This will always print”);
逻辑运算子
逻辑运算子可以组合其它逻辑(又称Boolean)值来产生新的Boolean值。PHP支援标准的逻辑运算(and、or、not和xor)前两个还有可替代的版本,如表7-1所示。
7-1 逻辑性运算符号
对于C语言程式设计师来说,一定很熟悉「&&」和「||」运算子。「!」运算子通常称为「NOT」,原因很明显。
下面的运算式是逻辑运算子的范例:
(($statement_1 && $statement_2)||
($statement_1 && ! $statement_2)||
(! $statement_1 && $statement_2)||
(! $statement_1 && ! $statement_2)||
这是一种「同义反复」,是指无论叙述的变数值是什么,结果都为真。两个变数的真正值有四种可能的组合,其中每个都由一个「&&」运算子表示。这四种中必然有一种为真,因为它们是用「||」运算子连结在一起的。整个运算式必然为真。
下面例子所使用的xor是更巧妙的「同义反复」示范:
(($statement_1 and $statement_2 and
$statement_3) xor
((! ( $statement_1 and $statement_2)) or
(! ( $statement_1 and $statement_3)) or
(! ( $statement_2 and $statement_3))))
这个运算式的含义是:「给定三个叙述语句,只能以生下面的这两种情况之一:若非氖的三个叙述都有为真,就会是有一对叙述不为真。」
逻辑运算子的优先顺序
与其它任何一种运算子相比,有些逻辑运算子的优先顺序更高,但都还可以使用圆括弧来改变优先顺序。以优先顺序的高低排列逻辑运算子的顺序比其它的低得多,因此指定运算子(=)比「and」绑得更紧,但比「&&」绑得松一些。
在http://www.php.net/线上手册中有一份运算子的优先顺序和关联性的完整列表。
逻辑运算子的短路
Boolean运算子有个非常便利顺手的特质就是从左到右结合,并能够设计让它「短路(short-circuit)」,如果第一个参数确定为真值,则根本不必再继续计第二个参数。例如在确定两个数字的非常近似比率,但还要避免可能除以0的错误。首先进行测试,为确保除数不是0,使用「!=」(不等于的意思)运算子即可:
if ($denom != 0 && $numer/ $denome>2)
print(“More than twice as much!”);
在$denom为「0」的情况下,无论第二个运算式是真还是假,「&&」运算子应该传回「非真」值。因为短路特性,第二个运算式就不被求值了,因此避免错误的发生。在$denom不等于「0」的情况下,「&&」运算子没有足够的资讯取得真值的结论,因此对第二个运算子求值。
到目前为此,我们正式讨论过的是TRUE和FALSE常数,以及如何把它们组合起来,形成其它真与非真值。现在让我们来看看进行实际Boolean运算测试的运算子。
比较运算子
表7-2显示是比较运算子,可用于数字或字串(请注意「非整数得比较小心」的说明)。
表7-2 比较运算符号
下面有些例子,是一些变数指定,后面跟随个永远为真的复合测试:
$three = 3;
$four = 4;
$my_pi = 3.14159;
If (($three = =$three) and
($four ===$four)and
($three !=$four)and
($three< $four) and
($three<=$four) and
($four>=$there) and
($three<=$three) and
($my_pi > $three) and
($my_pi <=$four))
print(“My faith in mathenmatics is restored! <BR>”);
else
print (“Sure you typed that right?<BR>”):
请注意个很常见的错误:把指定运算子(=)和比较运算子(==)弄混在一起。 「if($three = $four)..」这叙述很可能意外地把$three和$four设为相当变数(类型),如果$four为真值,则其测试结果将会为真!
运算子优先顺序
虽然过分依赖优先顺序则会使阅读程式码的人感到困惑,不过注意到比较运算子比Boolean运算子的优先顺序高还是很好的观念。请看下面的例子:
If ($small_num> 2 && $small_num <5)
它除了if必要的两个括弧外,不需要再多加括号。
字串比较
比较运算子既可以用于对字串,也可以用于比较数字(请留意「非常数得比较小心」的说明)。我们希望下面的程式输出显示出相应的例子:
if ((“Marx”<“Mary”)and
(“Mary”<“Marzipan”))
{
print(“Between Marx and Marzipan in the ”);
print(“dictionary, there was Mary .<BR>”);
}
非整数得比较小心
虽然比较运算子可以对数字或字串操作,但需要注意以下两方面的问题。
首先,对倍精度浮战火九(或一个整数与倍精度浮点数)进行大于小于的比较通常布言诉是安全的,但对倍精度浮点数进行相等的比较则会很危险,尤其是在对数字计算的结果进行比较时更要注意。这个问题在于「舍入错误」,它会使两个理论上相等的值稍微差了一点点。
其次,虽然比较运算子可以像对数字一样的运作方式串进行对比上的操作,但PHP的自动进行型别转换,因此有时会在把字串可解释为数字时得出违反常规的结果,例如,下面的程式码:
$strinhg_1 = “00008”;
$string _2 = “007”;
$string_3 = “00008-OK”;
If($string_2 <$string_1)
Print(“$string_2 is less than $string_1 <BR>”);
If($string_3< $string_2)
Print(“$string_3 is less than $string_2<BR>”);
If ($string_1 < $string_3)
Print(“$string_1 is less than $string_3<BR>”);
输出结论为:
007 is less than 00008 //数字比较
00008-OK is less than 007 //字串比对
00008 is less than 00008-OK//字串比对产生矛盾
如果可以的话,PHP会把字串参数化为数字,当比较的两侧都能够这样处理时,就会按数字比较,而不是按字母来比较。PHP4设计人员把它当作一种特性,而不是错误。要我们的观点来看,如果在比较时可能会被转化为数字的字串,最好是还是使用strcmp()函式是比较好的作法(请参照第十章)。
三元运算子
一种非常有用的结构是三元条件运算子,它扮演了Boolean运算子和「真值」分支结构之间的角色。它有作用是带三个运算式,使用第一个运算式的值来决定另外两个运算式中的哪一个才是所需的,求值后传回它的值,该运算子的语法如下:
test-expression ?yes-expression:no-expression
如果test-expression为真,这个运算式的值是yes-expression的结果,否则,就是no-expression的结果。
举例来看,下面的运算把$max_num指定为$first_num或$second_num,这取决于两个参数哪一个比较大:
$max_num = $first_num > $second_num ? $first_num:$second_num;
正如我们可以看出的,上面的叙述等于:
if ($first_num >$second_num )
$max_num = $first_num ;
else
$max_num = $second_num;
上述的例子若使用三元运算子则会更为简洁。
分支结构
分支的两种结构主要结构有if和switch.if是最常用的主要分支选择,通常也是所有人先学习的条件结构。在某些情况下switch也算是一种不错选取择,例如当基于单个值需要多个可能的分支时,或者当一系列if叙述非常麻烦时,非常适合使用switch。
if – else叙述
if的语法是:
if(test)
statement-1
或者,还可以带有可选取择的else分支:
if(test)
statement-1
else
statement-2
当处理了if叙述后,test运算会被评算求值,结果被解释为一个Boolean值。如果test不为真并且没有else子句,则接著执行if结构后面的下一条叙述.
请注意,语法中的「statement」可以是分号结尾的单一叙述,也可以是大括弧围住的叙述区块,或者是另一个条件结构(它本身可看成单一叙述)。条件可相互巢状嵌套任意深度。另外,Boolean运算式可以是真正的Boolean值(即TRUE、FALSE或Boolean运算子或函式的结果),也可以是能够解释为布林值的其它任何型别的值。
关于布林值的完整资讯,请参考第六章。简单地说,数字「0」、字串「“0”」和空字串「“”」为非真(false),其它所有值为真(true)。
下面的例子是输出显示两数之间的绝对差,其中示范了使用条件的巢状嵌套,也提及把测试结果解译为Boolean值:
if ($first - $second)
if ($first > $second)
{
$difference = $first - $second;
print(“The difference is $difference<BR>”);
}
else
{
$difference = $second - $first;
print(“The difference is $difference<BR>”);
}
else
print(“There is no difference<BR>”);
这段程式码依靠数字「0」解释为非真值,如果difference是「0」,则测试失败,输出显示「no difference」讯息,如果有差别,则执行一步的测试(这个例子是故意制作的,因为直接用像「$first != $second」这样的写法就与前述例子相同,且更容易理解)。
附加的else
此刻,以前使用过Pascal 的程式设计师可能很警觉到else部分有点怪,也就是说,else子句怎么知道它属于哪个if呢?其实规则很简单,几乎与Pascal外的其它所有语言中的情况相同。每个else与最接近的来匹配,当然首先要遵循大括弧限制住的范围。如果要确保if叙述维持独立、不需要与else匹配,那可以把它括在一对大弧中,如下所示:
if($num % 2 = =0)// $num为偶数?
{
if ($num > 2)
print(“num is not prime<BR>”);
else
print(“num is odd<BR>”);
如果$num是大于2的偶数,则程式码将印出「num is not prime」,如果$num为奇数,则印出「num is odd」,如果$num恰好为2,则什么也不显示。如果省略了大括弧,则else会附加到内层的if,因此如果$num等2,程式码也会错误地印出「num is odd」,如果$num的确是奇数,程式码则什么也不列印。
在本章的例子中,我们常使用取余数运算子(%),该运算子在第十二章中讲解。对于这些例子,读者只需要知道「$x % $y」为「0」的意思是「$x能被$y整除」。
Elseif
进行层叠型式的测试是很常见的,如下面的巢状嵌套if叙述所示:
if ($day == 5)
print(“Five golden rings<BR>”);
else
if ($day ==4)
print(“Four calling birds<BR>”);
else
if ($day ==3)
print(“Three French hens<BR>”);
else
if ($day ==2)
print(“Tow turtledoves<BR>”;
else
if ($day ==1)
print(“A partridge in a pear tree<BR>”);
这种型式很常见,有一个特殊的elseif结构来处理它。我们可以把前面的例子重写为:
if ($day == 5)
print(“Five golden rings<BR>”);
elseif($day == 4)
print(“Four calling birds<BR>”);
elseif($day == 3)
print(“Three French hens<BR>”);
elseif($day == 2)
print(“Two turtledoves<BR>”);
elseif($day ==1)
print(“A partridge in a pear tree<BR>”);
if、elseif……结构允许只在彼一个分支测试面成功时循序进行测试。从理论上说,这和前面的例子(一个结构带五个分支,而不是巢状嵌套的五个两头分支结构)在语法上是不同的,但行为是相同的。自己认为哪种好就用哪个吧。
分支和HTML模式
从前面几章中你已学过可以根据想要自由使用PHP标记在HTML模式和PHP模式间来回进行切换,无论如何都有是相当便利的。如果需要在页面中包括大区块的HTML文字,并且没有动态程式码或插入的变数,则切换回HTML模式并直接用文字来包含它,这样就比用print或echo发送它更为简单和有效。
不太明显的是,这个策略即使在本身条件结构内部也有效。也就是说,可以使用PHP决定传送什么HTML,然后透过临时切换回HTML模式来「传送」这些HTML。
举例来说,下面的程式码使用print叙述建构了一个完整的HTML网页(我们假设了一个female()的Boolean函式来测试它)。
<HTML><HEAD>
<?php
If(female())
{
Print(“TITLE>The women-only site</TITLE><BR>”);
print(“</HEAD><BODY>”);
print(“This site has been specially constructed”);
print(“for women only.<BR> No men allowed here!”);
}
Else
{
Print(“TITLE>The men-only site</TITLE><BR>”);
print(“</HEAD><BODY>”);
print(“This site has been specially constructed”);
print(“for men only.<BR> No women allowed here!”);
}
?>
</BODY></HTML>
我们可以不用所有这些print语法,而且两部分支内部都使用HTML程式码,例如:
<HTML><HEAD>
<?php
If(female())
{
?>
<TITLE>The women-only site</TITLE>
</HEAD><BODY>
This site has been specially constructed
For women only.<BR> No men allowed here!
<?php
}
Else
{
?>
<TITLE>The men-only site</TITLE><BR>
</HEAD><BODY>
This site has been specially constructed
for men only.<BR> No women allowed here!
<?php
}
?>
</BODY></HTML>
这个版本就更难阅读了,但它们唯一的差别是把每个print叙述换成了以PHP右标记(?>)开始,和以PHP左标记(
在本书是例子中,我们尽量避免含有这种类型的条件,因为为样对于PHP新手有些难以体会。但这不应该成为阻隔读者学习的理由,这种方式有自己的好处,其中包括执行速成度快(当处于HTML模式中,PHP引擎要做的所有事情就只是传递字元,并等待下一个PHP左标记,这样一定比解析再执行print叙述更快,尤其它们还包含有双引号的字串时更是如此)。
Switch
对于某种特定的多路分支类型来说,使用switch结构是比较有用的。Switch 不是根据任意逻辑运算式进行分支,而是按照单一运算式的值来选择不同的路径。Switch的语法如下,可选择性的部分置于中包括号([])中:
switch( expression)
{
case value -1:
statement-2
statement-2
…
[break;]
case value-2:
statement-3
statement-4
…
[break;]
…
[default:
default-statement]
}
expression可以是个变数,或者是任何其它类型的运算式,只要它求值结果为一个简单的值(整数、倍精度浮点数或字串)。该结构的执行方式是对 expression评算求值,然后测试其结果是否和某种情况的值相等。一旦找到一个匹配的值,后续的叙述则按顺序执行,直到特殊的叙述「break;」,或者直到switch结构结束(正如我们后面将要看到的break还可以在回圈结构中止与退出)。特殊的「default」标记可用在结构尾端,如果到它为止仍没有其它情况匹配,则它就是与运算式「匹配」的那一段。
例如,我们可以重写if-else的例子,如下所示:
switch($day)
{
case 5:
print(“Five golden rings<BR>”);
break;
case4:
print(“Four calling birds<BR>”);
break;
case 3:
print(“Three French hens<BR>”);
break;
case 2:
print(“Tow turtledoves<BR>”);
break;
default:
print(“A partridge in a pear tree<BR>”);
}
这个英文歌词的例子会在2-5于之中输出显示相对应的该行,而在其它日期则列印「A partridge in a pear tree」。
Switch 让人困扰的地方是,一个匹配的case后的所有case都将会执行,直到有break叙述来停止执行。在「partridge」的例子中,break叙述确保了一次只看到歌词中的一行。如果去掉break叙述,则会看到几行都被显示出来。
回圈
恭喜!你刚刚跨过了从script编制到「真正的程式设计」之间的界限了。到目前为止,我们看到的分支结构还算有用,但使用它们完成的计算还是有限的。另一方面,任何带有评算测试和无界限回圈的语言都能够比任何其它语言完成更好的功能,在电脑科学理论中,这一点是很确定的。在PHP中你也许并不需要编写一个C语言编译器,不过请记住这里没有什么天生的语言限制会阻止你这样做。