PHP学习笔记7:控制流
图源:php.net
if
php中常用的if
语法与C++或Java中的没有区别:
<?php
$a = 1;
if ($a < 5) {
echo "a < 5" . PHP_EOL;
} else if ($a == 5) {
echo "a == 5" . PHP_EOL;
} else {
echo "a > 5" . PHP_EOL;
}
// a < 5
其中else if
也可以写作elseif
,两者几乎没有区别。
php还有一种不常见的替代语法:
<?php
$a = 1;
if ($a < 5):
echo "a < 5" . PHP_EOL;
elseif ($a == 5):
echo "a == 5" . PHP_EOL;
else:
echo "a > 5" . PHP_EOL;
endif;
// a < 5
这种替代写法在风格上更像shell(比如bash),如果将其运用在html模版中时会比使用{}
顺眼一点:
<?php if (1<2): ?>
<h1>1<2</h1>
<?php endif; ?>
但老实说,我多年工作中从来没见人这么写过,事实上现在各种模版引擎都很成熟了,并不需要将php用于模版语言,就算是小型项目,使用传统语法也不会对有经验的php程序员造成影响,反而如果是混合着使用这种替代语法会让人困扰。
while
while
循环也没有太多可说的,其行为也与传统语言一致:
<?php
$arr = range(1, 20, 2);
$index = 0;
$len = count($arr);
while ($index < $len) {
echo $arr[$index] . ' ';
$index++;
}
echo PHP_EOL;
// 1 3 5 7 9 11 13 15 17 19
遍历数组的最优方式依然是
foreach
,这里仅为举例,下面介绍其他循环语句的示例同样如此,不再重复说明。
do while
do...while
与while
类似,区别在于do...while
中至少会执行一次循环块:
$arr = range(1, 20, 2);
$index = 0;
do {
if (count($arr) == 0) {
break;
}
echo $arr[$index] . ' ';
$index++;
} while ($index < count($arr));
// 1 3 5 7 9 11 13 15 17 19
for
for
语句是除了foreach
以外最常用的循环语句:
<?php
$arr = range(1, 10);
for ($i = 0; $i < count($arr); $i++) {
echo $arr[$i] . ' ';
}
echo PHP_EOL;
// 1 2 3 4 5 6 7 8 9 10
上边这个循环语句存在一个效率问题,即每次循环都会执行count($arr)
来计算数组长度,虽然数组长度在底层应该是保存为一个具体的值,count($arr)
理论上应该是一个常数级别的时间复杂度,但尽可能优化代码总是好的:
...
$len = count($arr);
for ($i = 0; $i < $len; $i++) {
...
}
我经常这么写,不过还可以:
...
for ($i = 0, $len = count($arr); $i < $len; $i++) {
...
}
这么做的优点是结构更清晰,因为理论上讲,$len
只会在循环语句中使用,是一个归属于for
语句的局部变量。但实际上这在php中并非如此,在循环体后依然可以访问到$len
,无论你将它定义在for
的初始化语句中还是for
之外。关于这点我已经在PHP学习笔记4:变量中变量作用域的部分讨论过了。
foreach
foreach
可以遍历iterable
类型的变量,这点在PHP学习笔记3:其它类型和类型声明中有过介绍:
$a = range(1, 10);
foreach ($a as $val) {
echo "{$val} ";
}
echo PHP_EOL;
// 1 2 3 4 5 6 7 8 9 10
foreach ($a as $key => $val){
echo "{$key}:{$val}, ";
}
echo PHP_EOL;
// 0:1, 1:2, 2:3, 3:4, 4:5, 5:6, 6:7, 7:8, 8:9, 9:10,
这里再展示一个遍历生成器函数的示例:
function get_fibnaci(): iterable
{
yield 1;
yield 1;
yield 2;
yield 3;
yield 5;
}
foreach (get_fibnaci() as $num) {
echo "{$num} ";
}
echo PHP_EOL;
遍历的时候可以利用索引结合[]
修改数组的值:
require_once "../util/array.php";
$a = range(1, 10);
foreach ($a as $key => $val) {
$a[$key] = 2 * $val;
}
print_arr($a);
// [0:2, 1:4, 2:6, 3:8, 4:10, 5:12, 6:14, 7:16, 8:18, 9:20]
不过有更简单的方式:
require_once "../util/array.php";
$a = range(1, 10);
foreach ($a as &$val) {
$val = 2 * $val;
}
unset($val);
print_arr($a);
// [0:2, 1:4, 2:6, 3:8, 4:10, 5:12, 6:14, 7:16, 8:18, 9:20]
需要注意的是,使用foreach
遍历数组时使用的引用变量&$val
应当在循环结束后立即使用unset()
消除。否则可能会在之后使用同样名称的循环变量时产生一些问题:
require_once "../util/array.php";
$a = range(1, 10);
foreach ($a as &$val) {
$val = 2 * $val;
}
print_arr($a);
// [0:2, 1:4, 2:6, 3:8, 4:10, 5:12, 6:14, 7:16, 8:18, 9:20]
$b = [1, 1, 1];
foreach ($b as $val) {;
}
print_arr($a);
// [0:2, 1:4, 2:6, 3:8, 4:10, 5:12, 6:14, 7:16, 8:18, 9:1]
第二个foreach
循环中,$val
并非一个新建的变量,而是由之前循环建立的,而且这是一个引用变量,当前指向的是$a[9]
的引用,所以foreach ($b as $val)
的一个副作用是,每次循环都会将引用变量指向的变量值用$b
当前元素改写。所以在第二次循环结束后,$a[9]
的值当变成$b[2]
的值,也就是1
。
有时候可以使用一些简单的结构来表示一些隐藏信息,而不是使用完整的索引:
<?php
$students = array(
array("Li lei", 20),
array("Xiao Ming", 15),
array("Jack Chen", 10),
);
foreach ($students as $std) {
$name = $std[0];
$age = $std[1];
echo "Student(name:{$name}, age:{$age})" . PHP_EOL;
}
但就像上面代码展示的那样,此时需要使用下标来明确指定对应的数据,稍显麻烦。在Python和Go中,有一种更简便的方式:
students = [("Li Lei", 20), ("Xiao Ming", 20), ("Jack Chen", 10)]
for name, age in students:
print("student(name:{:s}, age:{:d})".format(name, age))
# student(name:Li Lei, age:20)
# student(name:Xiao Ming, age:20)
# student(name:Jack Chen, age:10)
在Python中,for name,age in students
这种方式叫做“解包”,就是将每一个迭代的students
元素从元组分解到对应的单独变量。
php也提供类似的方式:
<?php
$students = array(
array("Li lei", 20),
array("Xiao Ming", 15),
array("Jack Chen", 10),
);
foreach ($students as list($name, $age)) {
echo "Student(name:{$name}, age:{$age})" . PHP_EOL;
}
// Student(name:Li lei, age:20)
// Student(name:Xiao Ming, age:15)
// Student(name:Jack Chen, age:10)
这里的list()
虽然看起来很像是一个函数,但本质上并不是,它是一个语法结构。可以将list
理解为一个特殊的,需要和()
结合使用的关键字。
和Python的解包语法类似,使用list
时也可以进行缺省:
...
foreach ($students as list($name,)) {
echo "Student(name:{$name}" . PHP_EOL;
}
// Student(name:Li lei
// Student(name:Xiao Ming
// Student(name:Jack Chen
需要注意的是,php的list
和Python的解包语法相比,更死板:
$students = array(
array("Li lei", 20, "3年级", "2班", "swim"),
array("Xiao Ming", 15, "5年级", "6班", "draw"),
array("Jack Chen", 10, "7年级", "2班", "music"),
);
foreach ($students as list($name,, $favorite)) {
echo "Student(name:{$name}, favorite:{$favorite}" . PHP_EOL;
}
// Student(name:Li lei, favorite:3年级
// Student(name:Xiao Ming, favorite:5年级
// Student(name:Jack Chen, favorite:7年级
可以看到,list
产生的结果是和数组中的元素位置完全对应的,并不能像Python那样通过一些特殊语法获取头部和尾部的信息:
students = [("Li Lei", 20, "四年级", "3班", "swim"),
("Xiao Ming", 20, "六年级", "8班", "music"),
("Jack Chen", 10, "三年级", "2班", "draw")]
for name, *_, favorite in students:
print("student(name:{:s}, favorite:{:s})".format(name, favorite))
# student(name:Li Lei, favorite:swim)
# student(name:Xiao Ming, favorite:music)
# student(name:Jack Chen, favorite:draw)
不管怎么说,妥善地使用list
语法会让一些遍历代码简洁很多。
更多list
的使用说明见官方手册list。
break
break
可以让代码从循环或者switch
结构中跳出。
这里以一个产生圣诞树字符图形的代码进行说明:
<?php
$a = range(1, 10);
foreach ($a as $val) {
for ($i = 0; $i < $val; $i++) {
echo '*';
}
echo PHP_EOL;
}
// *
// **
// ***
// ****
// *****
// ******
// *******
// ********
// *********
// **********
如果需要限定产生的*
形字符的最大长度,可以:
$a = range(1, 10);
foreach ($a as $val) {
for ($i = 0; $i < $val; $i++) {
if ($i >= 6) {
break;
}
echo '*';
}
echo PHP_EOL;
}
// *
// **
// ***
// ****
// *****
// ******
// ******
// ******
// ******
// ******
这样做只会跳出里层的循环,所以最后依然会以最大长度输出几行*
。如果要让程序在超过最大长度后直接结束输出,可以:
<?php
$a = range(1, 10);
foreach ($a as $val) {
for ($i = 0; $i < $val; $i++) {
if ($i >= 6) {
break 2;
}
echo '*';
}
echo PHP_EOL;
}
// *
// **
// ***
// ****
// *****
// ******
// ******
当然这并非唯一的做法,仅用于演示。
就像示例代码中展示的,break
语句可以追加数字,以表明要跳出的循环层数,默认情况下仅会跳出一层循环,相当于break 1
。
continue
php中continue
的基本用法与其它语言相同,这里不做过多介绍。比较特别的是,continue
和break
一样,也可以接受一个正整数,可以指定跳出若干层循环体后再进入下一次循环:
$a = range(1, 10);
foreach ($a as $val) {
echo PHP_EOL;
for ($i = 0; $i < $val; $i++) {
if ($i >= 3 && $val % 2 == 0) {
continue 2;
}
echo '*';
}
}
//
// *
// **
// ***
// ***
// *****
// ***
// *******
// ***
// *********
// ***
现在的图形更像圣诞树了:)
switch
switch
的用法完全和C++/Java保持一致:
<?php
function get_response(string $request): string
{
$response = "";
switch ($request) {
case "hello":
case "你好":
$response = "hello";
break;
case "how are you":
$response = "how are you";
break;
case "bye":
$response = "bye";
break;
default:
$response = "I don't known";
}
return $response;
}
echo get_response("hello") . PHP_EOL;
echo get_response("你好") . PHP_EOL;
echo get_response("bye") . PHP_EOL;
// hello
// hello
// bye
match
match
是php8.0.0新加入的语法结构,其基本的语法规范是:
<?php
$return_value = match (subject_expression) {
single_conditional_expression => return_expression,
conditional_expression1, conditional_expression2 => return_expression,
};
和switch
类似,match
也是罗列多个匹配条件进行匹配,并且每个匹配条件对应一个匹配后会被执行的表达式。不同的是,match
中匹配条件可以是复杂的表达式,而不仅仅局限于简单的基础类型数据。
此外,match
结构会返回一个结果,该结果是匹配到的条件对应的表达式执行后产生的。
和switch
一样,match
的匹配条件也是顺序执行,不过需要注意的是,如果某个条件被匹配,之后的匹配条件中的表达式都不会被执行:
<?php
function cond_func1()
{
echo "cond_func1 is called" . PHP_EOL;
return 1;
}
function cond_func2()
{
echo "cond_func2 is called" . PHP_EOL;
return 2;
}
function cond_func3()
{
echo "cond_func3 is called" . PHP_EOL;
return 3;
}
function match_func(int $num): string
{
return match ($num) {
cond_func1() => 'func1',
cond_func2() => 'func2',
cond_func3() => 'func3',
};
}
match_func(1);
// cond_func1 is called
match_func(2);
// cond_func1 is called
// cond_func2 is called
match_func(3);
// cond_func1 is called
// cond_func2 is called
// cond_func3 is called
match
中应当至少有一个条件被匹配到,否则会产生一个UnhandledMatchError
类型的异常:
match_func(4);
// Fatal error: Uncaught UnhandledMatchError: Unhandled match case 4 in ...
这个问题可以使用default
来避免:
...
function match_func(int $num): string
{
return match ($num) {
cond_func1() => 'func1',
cond_func2() => 'func2',
cond_func3() => 'func3',
default => 'no matched func',
};
}
...
match_func(4);
// cond_func1 is called
// cond_func2 is called
// cond_func3 is called
可以将match
的“主题”设置为true
,此时match
的作用将超脱“匹配”这个功能,而是变为纯粹地依次检查条件表达式的值是否为true
。利用这个特点,我们可以用match
实现一些本来可能要使用if...elif...
构建的复杂语句:
<?php
function get_response(string $request): string
{
return match (true) {
str_contains($request, 'hello') || str_contains($request, '你好') => 'hello',
str_contains($request, 'bye') || str_contains($request, '再见') => 'bye',
default => "I don't known",
};
}
echo get_response("hello, how are you.").PHP_EOL;
// hello
echo get_response("你好,吃饭了吗?").PHP_EOL;
// hello
echo get_response("bye.").PHP_EOL;
// bye
再看一个根据给定成绩返回分数等级的例子:
<?php
function get_grade(int $mark): string
{
return match (true) {
$mark < 60 => 'D',
$mark >= 60 && $mark < 80 => 'C',
$mark >= 80 && $mark < 90 => 'B',
$mark >= 90 && $mark <= 100 => 'A',
default => 'Error',
};
}
echo get_grade(55).PHP_EOL;
// D
echo get_grade(70).PHP_EOL;
// C
echo get_grade(90).PHP_EOL;
// A
return
return
语句的作用同样无需过多赘述,唯一需要阐述的是,php作为一个脚本语言,return
同样可以在最外层的脚本全局作用域内使用:
<?php
// a.php
$num = 1;
if ($num < 5){
return "\$num < 5";
}
else{
return "\$num >= 5";
}
echo "this will not be executed".PHP_EOL;
另一个脚本b.php
引用a.php
,并输出结果:
<?php
// b.php
$result = include("./a.php");
echo $result.PHP_EOL;
不过这样的用法并不常见,至少我从来没见过。
require
php使用inlude
或require
来加载别的代码。include
和require
的功能几乎完全一样,唯一的区别是如果加载目标代码失败,require
会产生E_ERROR
,并直接终止程序,后续代码将不会执行:
<?php
// a.php
echo "a.php is loaded." . PHP_EOL;
<?php
// b.php
require "./s.php";
require "./a.php";
// Warning: require(./s.php): Failed to open stream: No such file or directory in ... on line 2
// Fatal error: Uncaught Error: Failed opening required './s.php' (include_path='.;C:\php\pear') in ...
// Stack trace:
// #0 {main}
// thrown in ...b.php on line 2
include
则只会产生E_WARMING
,后续的代码依然会被执行:
<?php
// b.php
include "./s.php";
include "./a.php";
// Warning: include(./s.php): Failed to open stream: No such file or directory in ...b.php on line 3
// Warning: include(): Failed opening './s.php' for inclusion (include_path='.;C:\php\pear') in ...b.php on line 3
// a.php is loaded.
从更早发现可能的bug的角度看,更推荐使用require
。
在实际开发中,更多的是使用require_once
和include_once
,这样可以避免重复加载代码,并且还可以循环引用代码:
<?php
// a.php
require_once "./b.php";
class Student{
}
$teacher = new Teacher();
<?php
// b.php
require_once "./a.php";
class Teacher{
}
$std = new Student();
以前我以为循环引用是一件很普通的事,前一段时间学习了Python和Go才知道,很多语言都不支持循环引用,遇到类似的问题都要对代码进行拆分,以避免产生循环引用的问题。
关于加载代码的其它细节问题,这里不过多阐述,想了解的可以阅读官方手册include。
goto
php是支持goto
语法的,如果说C的指针是一个相当被诟病的设计,很多后来的借鉴于C的新语言都摒弃或者改善了对指针的支持,那么goto
就是一个彻彻底底的“恶魔”,你几乎找不出哪一门别的支持goto
的语言,当然,php
可以。
虽然php对goto
仅是有限支持,但我个人对此的建议是不要使用。
如果你依然感兴趣,可以前往官方文档goto。
谢谢阅读。