图源:
表达式
基础数据
基础类型的数据本身就是一个表达式:
echo 'hello'.PHP_EOL;
echo (1).PHP_EOL;
// hello
// 1
需要注意的是,echo 1.PHP_EOL;
会产生语法错误,因为整形值1
是不能和字符串连接的,但如果用()
将其包围,则(1)
就是一个明确的表达式,而表达式的结果虽然依然是整形值,但php解释器会将其隐式转换为字符串后进行字符串连接。
赋值语句
php中赋值语句也是表达式,换句话说,赋值语句的作用不仅是将一个值赋给一个变量,同时作为表达式,它本身还会产生一个值:
echo ($a=123).PHP_EOL;
echo $a.PHP_EOL;
// 123
// 123
并非所有语言的赋值语句都可以作为表达式使用,比如Python就不行,因此Python还在之前的某个版本加入了新特性——“赋值表达式”,有兴趣的可以阅读。
因为赋值语句是个表达式,所以可以在赋值语句中嵌入赋值语句:
$a = ($b = 123);
echo $a.PHP_EOL;
echo $b.PHP_EOL;
// 123
// 123
其实示例中的()
是不必要的:
$a = $b = 123;
echo $a.PHP_EOL;
echo $b.PHP_EOL;
// 123
// 123
这是因为表达式解析是从左到右的,且=
操作符是右结合的,所以php解释器自然而然会先执行$b=123
,然后将值代入表达式,执行$a=123
。
$a = $b = 123
这种写法也可以被称作“级联赋值”。
运算赋值
运算赋值是对赋值语句的一种简略写法,这广泛存在于几乎所有的编程语言:
<?php
$a = 1;
$a += 5;
echo $a.PHP_EOL;
// 6
示例中$a += 5
本质上等同于$a = $a + 5
,不过在写法上更为简洁。
赋值语句是表达式,同样的,运算赋值语句也是表达式:
$a = 5;
echo ($a += 6) . PHP_EOL;
// 11
函数调用
函数调用也是表达式,其值是返回值:
function test(){
return 5;
}
echo test().PHP_EOL;
// 5
自增、自减
可以看得出,php在语法上很多地方都参考了C/C++/Java这一系语言。php中存在自增和自减语句,且同样有前后的区别:
<?php
$num = 0;
echo (++$num).PHP_EOL;
// 1
echo ($num++).PHP_EOL;
// 1
echo $num.PHP_EOL;
// 2
用表达式解释前自增和后自增的区别就是:前自增是在表达式执行前让变量完成自增,后自增则是让自增发生在表达式执行之后。
三元运算符
类C语言中有一种奇怪的东西——“三元运算符”:
<?php
$a = 10;
$b = $a > 5 ? 1 : 0;
echo $b;
// 1
三元运算符?:
构成的其实也是一个表达式:
echo ($a > 5 ? 1 : 0) . PHP_EOL;
// 1
所以也会将其称之为“条件表达式”。
有些文章会将包含比较操作符(
>
/<=
等)的表达式称作条件表达式,但我更喜欢将其称作"bool表达式",将三元运算符构成的表达式称作条件表达式。
运算符
从概念上说,运算符是一种将多个表达式结合并构成一个更大表达式的符号。可以写作:
exp : [exp1 + exp2]
也就是说加运算符+
将两个子表达式exp1
和exp2
连接起来,构成了一个更大的表达式exp
。
运算符根据连接的表达式数量的不同,被划分为“一元运算符”、“二元运算符”、“三元运算符”。其中一元和二元运算符广泛存在于所有编程语言中,而三元运算符部分语言中并不存在。
优先级和结合律
运算符的优先级决定了那些运算符先执行哪些运算符后执行,当然基本的算术运算符的优先级是兼容数学领域的,这样符合直觉:
$a = 1 + 2 * 5;
echo $a . PHP_EOL;
// 11
和数学中一样,使用()
可以人为指定优先级:
$a = (1 + 2) * 5;
echo $a . PHP_EOL;
// 15
运算符的结合律则决定了使用同一运算符的情况下,要先执行运算符左边还是右边的运算:
$a = $b = 2;
echo $a.PHP_EOL;
echo $b.PHP_EOL;
// 2
// 2
这是一个前边举过的例子,赋值表达式是右结合律,所以其等价于$a = ($b = 2)
,即先执行=
右侧的部分。
结合优先级和结合律的规则,就可以知道一个表达式中应该先执行哪些部分,后执行哪些部分。
并不是所有运算符都有明确的结合律,有的是没有的,比如从php8.0.0开始,三元运算符的结合律从左结合变成了无,在这种情况下使用嵌套的多个三元运算符是不被允许的:
$a = true ? 0 : true ? 1 : 2; // (true ? 0 : true) ? 1 : 2 = 2 (PHP 8.0.0 前可用)
// Fatal error: Unparenthesized `a ? b : c ? d : e` is not supported.
因为php解释器不知道要如何分组这种表达式,即将表达式解析为true ? 0 : (true ? 1 : 2)
还是(true ? 0 : true) ? 1 : 2
。
当然这可以使用()
来避免语法错误:
$a = (true ? 0 : true) ? 1 : 2;
echo $a.PHP_EOL;
// 2
就像上面展示的那样,最好不要依赖于运算符的优先级和结合律来构建复杂的表达式,那样会产生一些潜在风险,更别说php随着版本更新多次改变了运算符的优先级和表达式定义。这样做最大的问题是代码的可读性很差。使用()
来明确表达式的优先级是个良好习惯。
完整的优先级和结合律列表可以查阅官方手册。
算术运算符
完整的算术运算符列表见。
和C++相比,php的除法运算符/
并不区分整数和浮点数,即使除数和被除数都是整数,运算也是除法而非C++中的整除:
$a = 1;
$b = 5;
echo ($a / $b) . PHP_EOL;
// 0.2
所以php中一般的除法运算结果都是浮点数,除非两个操作数都是整数,且结果恰好也是整数:
$a = 6;
$b = 2;
echo ($a / $b) . PHP_EOL;
// 3
如果需要在php中进行整除,应当使用 函数。
如果对浮点数进行模运算,会将操作数转换为整数后进行运算:
$a = 7.5;
$b = 2.2;
echo ($a % $b) . PHP_EOL;
// Deprecated: Implicit conversion from float 7.5 to int loses precision in ...
// Deprecated: Implicit conversion from float 2.2 to int loses precision in ...
// 1
可以看到,虽然会正常执行并产生结果,但会产生两条Deprecated
提醒,这意味着这种特性已经废弃,会在未来的php版本中删除,所以应当避免这种方式。如果要对浮点数取余,可以使用 函数。
此外,模运算中如果涉及负数,结果的正负号是与被除数的正负号一致的:
$a = -7;
$b = 2;
echo ($a % $b) . PHP_EOL;
// -1
$a = 7;
$b = -2;
echo ($a % $b) . PHP_EOL;
// 1
赋值运算符
特别的,因为php的字符串连接符是.
而非常见的+
,所以php的运算赋值符号中有一个独特的.=
:
$a = 'hello';
$a .= ' world';
$a .= '!';
$a .= PHP_EOL;
echo $a;
当然,这同样存在字符串连接中产生“中间字符串”过多,造成性能浪费的问题,所以构建长字符串通常使用下边的方式:
$longStr = array();
$longStr[] = 'hello';
$longStr[] = ' world';
$longStr[] = '!';
$longStr[] = PHP_EOL;
$a = implode('', $longStr);
echo $a;
// hello world!
php中没有指针,但存在引用赋值。对于对象以外的类型赋值,都是值拷贝:
<?php
require_once "../util/array.php";
$arr1 = array(1, 2, 3);
$arr2 = $arr1;
$arr2[0] = 99;
print_arr($arr1);
print_arr($arr2);
// [0:1, 1:2, 2:3]
// [0:99, 1:2, 2:3]
这里的工具函数print_arr
是我为了缩减数组打印结果编写的一个函数:
<?php
function print_arr(array $arr)
{
$ls = array();
$ls[] = '[';
$index = 0;
$len = count($arr);
foreach ($arr as $key => $value) {
$ls[] = "{$key}:{$value}";
if ($index < $len - 1) {
$ls[] = ', ';
}
$index++;
}
$ls[] = ']';
echo implode('', $ls) . PHP_EOL;
}
如果要在赋值后可以修改原始内容,则需要拷贝数据的引用,而不是值:
require_once "../util/array.php";
$arr1 = array(1, 2, 3);
$arr2 = &$arr1;
$arr2[0] = 99;
print_arr($arr1);
print_arr($arr2);
// [0:99, 1:2, 2:3]
// [0:99, 1:2, 2:3]
特别的,对于对象的拷贝都是引用,而非值:
<?php
class Student{
public $name;
public $age;
}
$std = new Student;
$std->name = 'Jack Chen';
$std->age = 20;
$std2 = $std;
$std2->name = 'Brus Lee';
echo "name:{$std->name}".PHP_EOL;
echo "name:{$std2->name}".PHP_EOL;
更多的赋值运算符说明见官方文档。
位运算
位运算,顾名思义就是对比特位进行的运算,也就是针对二进制整数的运算。
在php中,最常见的就是错误级别的相关内容:
$consts = get_defined_constants();
foreach ($consts as $constName => $constValue){
if (strpos($constName, 'E_')===0){
echo "name:{$constName} value:{$constValue}".PHP_EOL;
}
}
// name:E_ERROR value:1
// name:E_RECOVERABLE_ERROR value:4096
// name:E_WARNING value:2
// name:E_PARSE value:4
// name:E_NOTICE value:8
// name:E_STRICT value:2048
// ...
仔细观察预定义的错误级别相关常量,E_ERROR
是1
,E_WARNING
是2
,E_PARSE
是4
,E_NOTICE
是8
,它们都是二的倍数。之所以这么设计,是为了可以利用位运算来表示一些错误级别的“集合”,这也是位运算最实用的用途之一。
如果要让php程序只显示某种级别的错误,可以实用error_reporting
:
error_reporting(E_ERROR);
如果要显示多种级别的错误:
error_reporting(E_ERROR | E_NOTICE);
之所以可以这样,是因为位运算E_ERROR | E_NOTICE
的结果本身包含了两个二进制位,分别表示E_ERROR
和E_NOTICE
这两个错误级别:
<?php
require_once "../util/int.php";
print_binary(E_ERROR);
print_binary(E_NOTICE);
print_binary(E_ERROR | E_NOTICE);
// 0000000001
// 0000001000
// 0000001001
这里的print_binary
是我编写的一个用于输出二进制形式数字的辅助函数:
/**
* 以二进制形式打印数字(10位,0填充)
* @param int $binaryNum
*/
function print_binary(int $binaryNum): void
{
echo sprintf("%'.010b", $binaryNum) . PHP_EOL;
}
将结果用二进制表示就很容易理解了,E_ERROR
是b0001
,E_NOTICE
是b1000
,经过位或运算后的结果是b1001
,其第四位1表示结果中包含E_NOTICE
,第一位1表示包含E_ERROR
。这种用不同进位的0和1来表示多种信息的效果是相当巧妙的。
事实上这属于离散数学领域的内容,想详细了解理论方面知识的可以查阅相关资料。
同样,我们可以利用位运算的原理从一个包含多个信息的二进制数中"还原"信息:
<?php
require_once "../util/error.php";
test_error_include(E_ERROR | E_NOTICE);
test_error_include(E_NOTICE | E_ERROR | E_STRICT);
// E_ERROR,E_NOTICE
// E_ERROR,E_NOTICE,E_STRICT
这里的test_error_include
是一个检测错误级别经过位运算后包含的错误级别信息的工具函数:
<?php
function test_error_include(int $multiErros)
{
$errors = array();
if ($multiErros & E_ERROR) {
$errors[] = "E_ERROR";
}
if ($multiErros & E_PARSE) {
$errors[] = "E_PARSE";
}
if ($multiErros & E_NOTICE) {
$errors[] = "E_NOTICE";
}
if ($multiErros & E_STRICT) {
$errors[] = "E_STRICT";
}
if ($multiErros & E_WARNING) {
$errors[] = "E_WARNING";
}
if ($multiErros & E_CORE_ERROR) {
$errors[] = "E_CORE_ERROR";
}
if ($multiErros & E_USER_ERROR) {
$errors[] = "E_USER_ERROR";
}
echo implode(',', $errors) . PHP_EOL;
}
需要说明的是,这里仅包含了大多数常见错误级别,并非所有。还注意的是这里的多个if
可能同时满足,所以它们之间的关系是顺序的,而非if..else if...
。
可以看出,利用二进制位运算,原本可能需要传递一个数组到test_error_include
中,现在只需要直接传递位运算结果就能表示多个错误级别的集合。而内建函数error_reporting
采用的是相同的原理。
除了使用位或,还可以使用其它位运算符表示各种含义,比如:
// E_ERROR,E_NOTICE,E_STRICT
test_error_include(E_ALL & ~E_NOTICE); //E_NOTICE以外的错误级别
// E_ERROR,E_PARSE,E_STRICT,E_WARNING,E_CORE_ERROR,E_USER_ERROR
test_error_include(E_ALL ^ E_NOTICE); //E_NOTICE以外的错误级别
// E_ERROR,E_PARSE,E_STRICT,E_WARNING,E_CORE_ERROR,E_USER_ERROR
示例中的两种位运算其实是等价的,具体可以参考位运算原理和离散数学,这里不再详细说明。
按理来说其实也可以用下面来表示:
test_error_include(~E_NOTICE); //不推荐
// E_ERROR,E_PARSE,E_STRICT,E_WARNING,E_CORE_ERROR,E_USER_ERROR
但这里的~E_NOTICE
和E_ALL ^ E_NOTICE
的结果可能并不完全相同:
require_once "../util/int.php";
print_binary(~E_NOTICE);
// 1111111111111111111111111111111111111111111111111111111111110111
print_binary(E_ALL & ~E_NOTICE);
// 111111111110111
这是因为E_ALL
的高位会用0填充,它只会将“有意义”的低位(对应所有的错误级别)设置为1。
位运算还包含左移<<
和右移>>
运算,这里不再说明,具体可以阅读官方手册。
比较运算符
如果你是先学习了其它语言,尤其是强类型语言,使用php进行比较运算时需要注意,php会在“需要”时进行一些类型转换后进行比较,所以某些其它语言中不可能相等的结果在php中却是相等的:
var_dump(0 == "a");
// bool(false)
var_dump("1" == "01");
// bool(true)
var_dump("10" == "1e1");
// bool(true)
var_dump(100 == "1e2");
// bool(true)
因为这里的"1"
、"01"
、"1e1"
等都会被当做数字字符串来对待,在比较时会先转化为数字,然后比较。
而且这种结果还会因php版本的不同而不同,所以只要类型确定,应当使用===
而非==
进行比较:
var_dump(0 === "a");
// bool(false)
var_dump("1" === "01");
// bool(false)
var_dump("10" === "1e1");
// bool(false)
var_dump(100 === "1e2");
// bool(false)
很多开发者喜欢“引战”,会比较不同语言的差异评论优劣,其实语言特性往往是开发团队因为某些原因取舍的结果,这只是语言的一种“特性”,作为语言的使用者而言,其实是不存在优劣的。
就上面的相等运算来说,你可以认为这是一种php的缺陷,但也可以认为这是php灵活性的体现,可以实现一些别的语言中无法实现的效果。
这其中的重点在于你需要搞懂其中的细微差别,并时刻清楚你的代码会产生哪些后果。
如果你足够谨慎,还可以使用strcmp
函数来比较字符串:
var_dump(strcmp("1", "01") == 0);
// bool(false)
var_dump(strcmp("10", "1e1") == 0);
// bool(false)
php还有一个相当“花哨”的比较运算符<=>
,叫做“太空船”运算符,这个名字和“海象运算符”有的一拼。该运算符的描述是对于$a<=>$b
,如果$a
大于$b
,会返回一个大于0的数,如果$a
等于$b
,会返回一个小于0的数,如果$a
等于$b
,则会返回0。
其实这种规则和一些用于比较的函数的返回值很像,比如我们可以利用它来创建一个用于比较对象的函数:
<?php
class Student
{
public $name = "";
public $age = 0;
}
/**
* 比较两个student的大小
* @param Student $std1
* @param Student $std2
* @return int 如果std1大于std2,返回值大于0,如果...
*/
function compare_student(Student $std1, Student $std2): int
{
return $std1->age <=> $std2->age;
}
$std1 = new Student;
$std2 = new Student;
$std1->age = 20;
$std2->age = 15;
var_dump(compare_student($std1, $std2));
// int(1)
当然,对待“花哨”的运算符的最好方式是——不要使用。
同样的,想了解完整比较运算符列表,请移步官方手册。
错误控制运算符
php存在一个错误控制运算符@
,其作用是添加在表达式头部后,可以屏蔽其后表达式可能出现的错误提示。
这里用打开文件作为示例,当试图打开一个不存在的文件时:
<?php
$lines = file("file_no_exists");
// Warning: file(file_no_exists): Failed to open stream: No such file or directory in ...
使用@
屏蔽warning
信息,并在失败时打印自定义信息并退出:
<?php
$lines = @file("file_no_exists") || die("file is not exists, exit.");
// file is not exists, exit.
在php8.0以前,使用@
可能会屏蔽一些导致程序中断运行的致命错误提示,在8.0之后不会了。
需要注意的是,如果你设置了自定义的错误处理程序,@
依然会触发:
<?php
function my_error_handle($err_no, $err_msg, $filename, $linenum){
echo $err_msg.PHP_EOL;
}
set_error_handler("my_error_handle");
$lines = @file("file_no_exists") || die("file is not exists, exit.");
// file(file_no_exists): Failed to open stream: No such file or directory
// file is not exists, exit.
要想让@
对自定义错误处理程序同样生效,需要加入相应的判断:
function my_error_handle($err_no, $err_msg, $filename, $linenum){
if (!(error_reporting() & $err_no)){
return false;
}
echo $err_msg.PHP_EOL;
}
对于@
,我个人建议是,不要使用,不要使用,不要使用。
执行运算符
执行运算符"``"的作用是执行shell命令:
<?php
$result = `dir`;
echo $result . PHP_EOL;
// Volume in drive D is WorkSpace
// Volume Serial Number is EE20-29FD
// Directory of d:\workspace\http\php-notes\ch6
// 2021/12/04 15:26 <DIR> .
// 2021/12/04 15:26 <DIR> ..
// 2021/12/04 10:16 203 assignment.php
// ...
// 2021/12/04 11:25 317 priority.php
// 2021/12/04 11:26 63 priority2.php
// 2021/12/04 10:38 103 three.php
// 25 File(s) 5,203 bytes
// 2 Dir(s) 52,707,540,992 bytes free
这在某些时候很有用,比如编写一些运维使用的批处理脚本。但在大多数正规的Web开发中应当避免使用,因为shell命令都是平台相关的,不同的平台有不同的shell命令,且就算是同一平台,某些shell命令能否执行也取决于是否安装了某些必要的环境。
此外执行运算符也不是必须的,可以使用shell_exec
函数代替:
$result = shell_exec("dir");
我个人更推荐使用后者,过多“花哨”的运算符只会降低代码的可读性。
递增、递减运算符
前面已经讨论过递增、递减运算符,这里不再进行介绍。唯一需要说明的是,因为前自增后和自增结合复杂的表达式使用,往往会引起一些歧义和阅读困难,比如:
<?php
$a = 1;
$b = $a + ++$a + $a++;
echo $a.PHP_EOL;
echo $b.PHP_EOL;
// 3
// 6
编写这样的代码完全是自寻烦恼,应当尽量避免:
<?php
$a = 1;
$a++;
$b = $a + $a + $a;
$a++;
echo $a.PHP_EOL;
echo $b.PHP_EOL;
// 3
// 6
这大概是为什么有些语言(如Go)有后自增,但没有前自增。而另一些语言(如Python),没有任何自增运算符。
逻辑运算符
有的语言(如Python)使用单词作为逻辑运算符,有的语言(如C++)则使用符号,php都支持:
<?php
$a = 1;
$b = 5;
if ($a < $b && is_int($a)) {
echo "a < b" . PHP_EOL;
// a < b
}
if ($a < $b and is_int($a)) {
echo "a < b" . PHP_EOL;
// a < b
}
所以在php中&&
与and
是等价的,||
与or
是等价的。不过逻辑异或xor
并没有对应的符号表示的逻辑操作符:
var_dump(true xor true);
// bool(false)
var_dump(false xor false);
// bool(false)
var_dump(true xor false);
// bool(true)
var_dump(false xor true);
// bool(true)
完整的逻辑运算符列表见官方手册。
字符串运算符
用于字符串连接的运算符有两个:.
和.=
。在本文的前边已经有说明,所以不再阐述。
数组运算符
+
运算符和+=
运算符可以将两个数组进行合并:
<?php
require_once "../util/array.php";
$a = ['a1', 'a2', 'a3'];
$b = ['b1', 'b2'];
print_arr($a + $b);
// [0:a1, 1:a2, 2:a3]
print_arr($b + $a);
// [0:b1, 1:b2, 2:a3]
对于非关联数组,合并结果只会包含“多出来的索引对应的键值对”。事实上这个特征并不仅仅限于非关联数组,队员关联数组同样适用:
$a = ["banana" => "banana", "apple" => "apple"];
$b = ["banana" => "banana", "orange" => "orange"];
print_arr($a + $b);
// [banana:banana, apple:apple, orange:orange]
print_arr($b + $a);
// [banana:banana, orange:orange, apple:apple]
一般来说,很少在实际开发中使用+
操作符来合并数组,更常见的是使用数组函数:
print_arr(array_unique(array_merge($a, $b)));
// [banana:banana, apple:apple, orange:orange]
这种情况下可以不用考虑前后数组的key是否存在重叠的问题。
==
可以比较两个数组是否相等:
$a = ["banana" => "banana", "apple" => "apple"];
$b = ["apple" => "apple", "banana" => "banana"];
var_dump($a == $b);
// bool(true)
$a = [1,2];
$b = ["1","02"];
var_dump($a == $b);
// bool(true)
像上面展示的那样,键值对顺序不对也会被认为相等,此外特别要注意的是,在比较数组时,php解释器同样会进行某些隐式转换后比较,比如上边的数字字符串"1"
和"02"
就被转换为数字后进行比较,所以两个数组也是相等的。
如果要严格比较两个数组是否相等,需要使用===
:
$a = ["banana" => "banana", "apple" => "apple"];
$b = ["apple" => "apple", "banana" => "banana"];
var_dump($a === $b);
// bool(false)
$a = [1,2];
$b = ["1","02"];
var_dump($a === $b);
// bool(false)
在这种情况下,数组的键值对不仅要内容一致、类型一致,同时顺序也要一致才会被认为是相等的。
此外,==
的反面是!=
,而===
的反面是!==
。
类型运算符
使用类型运算符instanceof
可以判断对象是否为类的实例:
<?php
class MyClass
{
}
class NotMyClass
{
}
$a = new MyClass;
var_dump($a instanceof MyClass);
var_dump($a instanceof NotMyClass);
// bool(true)
// bool(false)
对继承关系也能很好的判断:
<?php
class ParentClass
{
}
class MyClass extends ParentClass
{
}
$a = new MyClass;
var_dump($a instanceof MyClass);
var_dump($a instanceof ParentClass);
// bool(true)
// bool(true)
因为判断的结果是bool值,所以可以结合逻辑运算符使用:
var_dump(!($a instanceof MyClass));
// bool(false)
也能判断接口:
<?php
interface MyInterface{
}
class MyClass implements MyInterface{
}
$a = new MyClass;
var_dump($a instanceof MyClass);
var_dump($a instanceof MyInterface);
// bool(true)
// bool(true)
如果instanceof
左边的操作数不是对象,结果是false
:
<?php
var_dump(1 instanceof stdClass);
var_dump("hello" instanceof stdClass);
var_dump(false instanceof stdClass);
// bool(false)
// bool(false)
// bool(false)
以上就是php表达式和运算符的全部内容,谢谢阅读。
往期内容
文章评论