PHP 基本语法 02|运算符与表达式:优先级里埋着历史

算术运算符

<?php
$a = 7; $b = 3;

var_dump($a + $b);        // int(10)
var_dump($a - $b);        // int(4)
var_dump($a * $b);        // int(21)
var_dump($a / $b);        // float(2.333...)  —— 除不尽就变 float
var_dump($a % $b);        // int(1)           —— 取模,操作数会先转成 int
var_dump($a ** $b);       // int(343)         —— 幂运算(PHP 5.6+)
var_dump(intdiv(7, 3));   // int(2)           —— 整除(PHP 7+)

两条容易踩的规则:

  • / 永远返回 float,除非两个操作数都是整数且能整除——不,更精确地说:PHP 的 / 在无法整除时返回 float,能整除时返回 int。6/2 是 int(3),7/3 是 float。想要整数结果用 intdiv()。
  • % 在 PHP 8 之前,除数为 0 会报警告并返回 false,PHP 8.0 起除零会抛 DivisionByZeroError。取模的符号跟随被除数:-7 % 3 是 -1。

浮点运算的老问题仍然在:

<?php
var_dump(0.1 + 0.2);              // float(0.30000000000000004)
var_dump(0.1 + 0.2 == 0.3);       // bool(false)
var_dump(abs((0.1 + 0.2) - 0.3) < PHP_FLOAT_EPSILON);  // bool(true) 推荐写法

金额计算不要用 float。用整数存"分",或者用 bcmath / gmp 扩展:

<?php
bcscale(2);
echo bcadd("19.90", "29.90");   // 49.80

字符串运算符:. 和 .=

<?php
$s = "Hello" . " " . "World";   // 拼接
$s .= "!";                       // 追加
echo $s;                         // Hello World!

这里有一条 PHP 8.0 的重要变更:. 的优先级原来比 +/- 高,导致 "总和:" . 1 + 2 会算成 ("总和:" . 1) + 2。PHP 8.0 把 . 的优先级降到了比 +/- 低,所以现在 "x" . 1 + 2 会算成 "x" . (1 + 2)。如果你从 PHP 7 迁移代码,这里必须检查——以前能跑错的表达式现在会跑得不一样。

保险做法:拼接复杂的表达式时一律加括号。

赋值运算符

<?php
$a = 5;
$a += 3;    // = 8
$a -= 3;    // = 5
$a *= 2;    // = 10
$a /= 2;    // = 5
$a %= 3;    // = 2
$a **= 3;   // = 8
$a .= "x";  // = "8x"
$a ??= 10;  // 只有当 $a 为 null 未赋值时才赋(PHP 7.4+)

??= 很实用,用来做"默认值只设一次":

<?php
$config['cache'] ??= 'redis';

比较运算符:这是 PHP 最难的一块

运算符 含义
== 松散比较,会做类型转换
=== 严格比较,类型 + 值
!= / <> 松散不等
!== 严格不等
<=> 太空船,返回 -1 / 0 / 1(PHP 7+)
?: 三元缩写,$a ?: $b 等价于 $a ? $a : $b

<=> 在排序回调里非常好用:

<?php
$users = [
    ['name' => 'bob',   'score' => 80],
    ['name' => 'alice', 'score' => 95],
    ['name' => 'carl',  'score' => 80],
];

usort($users, fn($x, $y) => $y['score'] <=> $x['score']);   // 分数降序

一条口决记住:只要涉及用户数据、涉及业务判断、涉及 in_array/array_search,全部用 ===。松散比较只在"数值比较且确定类型"时用。

逻辑运算符:&& 与 and 的优先级差异

<?php
$result = true && false;    // false
$result = true and false;   // true  ← 因为 = 的优先级高于 and!

= 的优先级比 and / or / xor 高,所以 $a = true and false 会被解析成 ($a = true) and false,$a 是 true。

这组运算符是给"控制流"用的,不是给赋值用的:

<?php
$fp = fopen($file, 'r') or die("打不开文件");   // 常见写法

我的建议:永远用 && / || / !,and/or 只有在上面这种刻意利用优先级的场景才用,且要加注释。

PHP 8 的 && / || 还带短路:false && foo() 不会执行 foo()。这可以用来做条件调用:

<?php
$user && sendMail($user);   // 有 user 才发信

但注意 $user 必须是布尔语义,用 if 更清晰。

位运算

<?php
$a = 0b1100;   // 12
$b = 0b1010;   // 10

var_dump($a & $b);    // 8   按位与
var_dump($a | $b);    // 14  按位或
var_dump($a ^ $b);    // 6   按位异或
var_dump(~$a);        // -13 按位取反
var_dump($a << 1);    // 24  左移
var_dump($a >> 1);    // 6   右移

位运算最实际的用途是权限标志位:

<?php
const PERM_READ    = 1 << 0;   // 1
const PERM_WRITE   = 1 << 1;   // 2
const PERM_DELETE  = 1 << 2;   // 4

$perm = PERM_READ | PERM_WRITE;          // 3
var_dump($perm & PERM_DELETE);           // 0,没有删除权限
var_dump(($perm & PERM_READ) !== 0);     // true
$perm &= ~PERM_WRITE;                    // 取消写权限

比存字符串数组省事得多,一个整数就装下所有标志。

错误控制符 @

<?php
$content = @file_get_contents($path);   // 抑制警告

@ 会临时把错误报告级别降到 0。它能用,但不要滥用:它会连致命错误之外的所有提示一起吞掉,调试时非常难受。而且 PHP 8 之后 @ 只作用于"同一个表达式内",行为比旧版更窄。

更好的替代:

<?php
if (is_readable($path)) {
    $content = file_get_contents($path);
}

执行运算符

反引号执行 shell 命令:

<?php
$out = `ls -l`;
echo $out;

等价于 shell_exec()。除非你非常清楚输入不可信会怎样,否则不要用——任何拼接进命令的变量都是命令注入。

递增递减

<?php
$a = 5;
echo $a++;   // 5,先返回再加
echo $a;     // 6
echo ++$a;   // 7,先加再返回

$s = "a";
$s++;        // "b" —— 字符串也能 ++(Perl 遗留行为)
$s = "z";
$s++;        // "aa"

字符串自增是 PHP 的历史遗留特性,看着神奇,实际项目里基本是 bug 源。数字溢出的行为也不一致:PHP_INT_MAX++ 会变成 float。

null 安全运算符 ?->

PHP 8.0 引入,避免"对象可能为 null"的层层判断:

<?php
// 旧写法
$city = $user && $user->address ? $user->address->city : null;

// PHP 8
$city = $user?->address?->city;   // 中间任一环为 null 就整体返回 null

比 isset() 链干净太多,这是 PHP 8 里最实用的一处语法加法。

注意它只能用在对象访问上,数组还得用 ??:

<?php
$city = $obj?->address?->city;          // 对象链
$city = $arr['address']['city'] ?? null; // 数组链

match:比 switch 干净的表达式(PHP 8.0)

<?php
$code = 404;

$text = match (true) {
    $code >= 500 => "服务端错误",
    $code >= 400 => "客户端错误",
    $code >= 300 => "重定向",
    default      => "其他",
};

match 与 switch 的三点本质差别:

  1. match 是表达式,直接有返回值,不用写 break。
  2. match 用严格比较(===),不会再出现 "1" 匹配到 1 的怪事。
  3. 没有 default 且没匹配上时,会抛 UnhandledMatchError——这是它最大的优点:漏掉分支会立刻炸,而不是静默返回 null。

优先级速查(常犯错的三组)

<?php
// 1) 字符串拼接 vs 加减 —— PHP 8 已改,但仍建议加括号
echo "结果:" . (1 + 2);     // 明确

// 2) 三元嵌套 —— 从 PHP 8 起必须加括号,否则语法错误
$v = $a ? $b : ($c ? $d : $e);   // 合法
// $v = $a ? $b : $c ? $d : $e;  // PHP 8 报 ParseError

// 3) 赋值 vs and/or
$ok = foo() or bar();   // 先赋值,再判断 → 等价 ($ok = foo()) or bar()

最稳的做法不是背优先级表,是加括号。括号不花钱,读代码的人省十分钟。

坑的清单

  • 0.1 + 0.2 != 0.3,金额别用 float。
  • $a = true and false,$a 是 true。
  • PHP 8 里 . 与 + 的优先级反转,迁移必查。
  • "1" == "01" 是 true,字符串数字比较会转数值。
  • 除零在 PHP 8 抛异常,不再返回 false。
  • @ 抑制不等于错误被处理,只是被吞了。