图源:php.net
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
循环也没有太多可说的,其行为也与传统语言一致:
<?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
与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
语句是除了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
可以遍历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
可以让代码从循环或者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
。
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
的用法完全和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
是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
语句的作用同样无需过多赘述,唯一需要阐述的是,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;
不过这样的用法并不常见,至少我从来没见过。
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。
php是支持goto
语法的,如果说C的指针是一个相当被诟病的设计,很多后来的借鉴于C的新语言都摒弃或者改善了对指针的支持,那么goto
就是一个彻彻底底的“恶魔”,你几乎找不出哪一门别的支持goto
的语言,当然,php
可以。
虽然php对goto
仅是有限支持,但我个人对此的建议是不要使用。
如果你依然感兴趣,可以前往官方文档goto。
谢谢阅读。