PHP学习笔记7:控制流

PHP学习笔记7:控制流

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...whilewhile类似,区别在于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的基本用法与其它语言相同,这里不做过多介绍。比较特别的是,continuebreak一样,也可以接受一个正整数,可以指定跳出若干层循环体后再进入下一次循环:

$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使用inluderequire来加载别的代码。includerequire的功能几乎完全一样,唯一的区别是如果加载目标代码失败,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_onceinclude_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

谢谢阅读。

往期内容

上一篇:使用Gitlab一键安装包后的日常备份恢复与迁移


下一篇:诚实的力量。Paul Graham (Y Combinator 创始人)关于诚实的评论。