Skip to content

表达式求值

表达式求值是 PHP 执行代码的核心过程。当 PHP 解析器遇到一个表达式时,它需要确定表达式的类型、计算其值,并在必要时进行类型转换。理解求值规则、运算符优先级和短路求值机制,有助于编写正确且高效的代码。

前置知识

  • 已掌握 表达式基础 的概念
  • 了解 PHP 的基本数据类型
  • 熟悉常用运算符的用法

基础概念

表达式求值是指 PHP 计算表达式并得到一个值的过程。求值涉及多个方面:类型确定、运算执行、类型转换和结果返回。

求值的基本规则

  1. 从内到外:括号内的表达式先求值
  2. 根据优先级:不同运算符有不同的优先级
  3. 根据结合方向:相同优先级的运算符按结合方向求值
  4. 短路求值:逻辑运算符可能跳过部分表达式
  5. 类型自动转换:必要时自动转换操作数的类型

Truthy 与 Falsy 值

当表达式用于布尔上下文(如 if 条件、&&/|| 操作)时,PHP 会将值自动转换为布尔值。

Falsy 值

以下值在布尔上下文中被转换为 false

值类型Falsy 值
布尔值false
整数0-0
浮点数0.0-0.0
字符串""(空字符串)、"0"
数组[](空数组)
nullnull
SimpleXML从空元素创建的对象

Truthy 值

所有不是 Falsy 的值都是 Truthy:

php
<?php
declare(strict_types=1);

// Truthy 值示例
$truthyValues = [
    true,                    // 布尔 true
    1, -1, 42,               // 非零整数
    3.14, -0.001,            // 非零浮点数
    "hello", " ", "false",    // 非空字符串("0"除外)
    [1, 2], ["key" => "val"], // 非空数组
    new stdClass(),           // 非空对象
];

// Falsy 值示例
$falsyValues = [
    false,          // 布尔 false
    0, -0,          // 零整数
    0.0, -0.0,      // 零浮点数
    "",             // 空字符串
    "0",            // 字符串 "0"
    [],             // 空数组
    null,           // null
];

空合并运算符 ?? 与 Falsy 值

?? 只检查是否为 null,不会将 Falsy 值视为"无值"。这与 ?:(Elvis 运算符)不同:

php
<?php
declare(strict_types=1);

// ?? 只检查 null
$a = 0 ?? "default";       // 0(0 不是 null)
$b = "" ?? "default";      // ""("" 不是 null)
$c = "0" ?? "default";     // "0"("0" 不是 null)
$d = null ?? "default";    // "default"(null 被替换)

// ?: 检查 truthy/falsy
$e = 0 ?: "default";       // "default"(0 是 falsy)
$f = "" ?: "default";      // "default"("" 是 falsy)
$g = "0" ?: "default";     // "default"("0" 是 falsy)
$h = 1 ?: "default";       // 1(1 是 truthy)

echo "a={$a}, b={$b}, c={$c}, d={$d}\n";
echo "e={$e}, f={$f}, g={$g}, h={$h}\n";

类型自动转换规则

转换为布尔值

php
<?php
declare(strict_types=1);

// 以下是 falsy,转换后为 false
var_dump((bool)0);         // bool(false)
var_dump((bool)0.0);       // bool(false)
var_dump((bool)"");        // bool(false)
var_dump((bool)"0");       // bool(false)
var_dump((bool)[]);        // bool(false)
var_dump((bool)null);      // bool(false)

// 以下是 truthy,转换后为 true
var_dump((bool)1);         // bool(true)
var_dump((bool)-1);        // bool(true)
var_dump((bool)"hello");   // bool(true)
var_dump((bool)"false");   // bool(true)
var_dump((bool)[1]);       // bool(true)

转换为整数

php
<?php
declare(strict_types=1);

// 布尔值
var_dump((int)true);    // int(1)
var_dump((int)false);   // int(0)

// 浮点数(向零截断)
var_dump((int)3.9);     // int(3)
var_dump((int)-3.9);    // int(-3)

// 字符串
var_dump((int)"42");    // int(42)
var_dump((int)"3.14");  // int(3)
var_dump((int)"abc");   // int(0)
var_dump((int)"42abc"); // int(42)

// null
var_dump((int)null);    // int(0)

// 数组
var_dump((int)[5]);      // int(1)(非空数组)
var_dump((int)[]);       // int(0)(空数组)

转换为字符串

php
<?php
declare(strict_types=1);

// 数值
var_dump((string)42);      // string(2) "42"
var_dump((string)3.14);    // string(4) "3.14"

// 布尔值
var_dump((string)true);    // string(1) "1"
var_dump((string)false);   // string(0) ""

// null
var_dump((string)null);    // string(0) ""

// 数组
var_dump((string)[1, 2]);  // string(5) "Array"(直接转换产生警告)

详细说明

运算符优先级对求值的影响

优先级决定了表达式中各部分的计算顺序。高优先级的运算符先执行:

php
<?php
declare(strict_types=1);

// 乘法优先级高于加法
echo 1 + 5 * 3 . "\n";       // 16(不是 18)

// 括号改变优先级
echo (1 + 5) * 3 . "\n";     // 18

// 比较运算符的优先级
// && 优先级高于 ||
// 等价于 ($a && $b) || $c
$a = true;
$b = false;
$c = true;
$result = $a && $b || $c;
echo $result ? "true" : "false"; // true

// 等价于 $a && ($b || $c)
$result2 = $a && ($b || $c);
echo $result2 ? "true" : "false"; // true

and/or vs &&/|| 的优先级差异

关键区别

and/or 的优先级低于赋值运算符 =,而 &&/|| 的优先级高于赋值运算符。这是常见的 bug 来源。

php
<?php
declare(strict_types=1);

// && 优先级高于 =
$result = true && false;
var_dump($result); // bool(false)
// 等价于:$result = (true && false)

// and 优先级低于 =
$result = true and false;
var_dump($result); // bool(true)
// 等价于:($result = true) and false

// || 优先级高于 =
$flag = false || true;
var_dump($flag); // bool(true)

// or 优先级低于 =
$flag = false or true;
var_dump($flag); // bool(false)
// 等价于:($flag = false) or true

短路求值详解

短路求值(Short-circuit evaluation)是逻辑运算符的重要特性:

php
<?php
declare(strict_types=1);

// || 短路:左侧为 true 则跳过右侧
function logMessage(string $msg): void
{
    echo "Logging: {$msg}\n";
}

$debug = true;
$debug || logMessage("Debug mode is OFF");
// logMessage 不会被调用($debug 为 true)

// && 短路:左侧为 false 则跳过右侧
function checkPermission(): bool
{
    echo "Checking permission...\n";
    return true;
}

$hasAccess = false;
$hasAccess && checkPermission();
// checkPermission 不会被调用($hasAccess 为 false)

// 实用模式:安全访问嵌套数据
$user = ["profile" => ["name" => "Alice"]];
$name = isset($user["profile"]) && isset($user["profile"]["name"])
    ? $user["profile"]["name"]
    : "Unknown";
echo $name . "\n"; // Alice

// PHP 7.0+ 空合并运算符更简洁
$name = $user["profile"]["name"] ?? "Unknown";
echo $name . "\n"; // Alice

严格比较 vs 宽松比较

PHP 有两种比较方式:==(宽松比较,允许类型转换)和 ===(严格比较,类型和值都相同):

php
<?php
declare(strict_types=1);

// == 宽松比较(类型转换后比较)
var_dump(0 == false);       // bool(true)
var_dump("" == false);       // bool(true)
var_dump("0" == false);      // bool(true)
var_dump("1" == true);       // bool(true)
var_dump(0 == "0");          // bool(true)
var_dump("" == null);        // bool(true)

// === 严格比较(不进行类型转换)
var_dump(0 === false);      // bool(false)
var_dump("" === false);      // bool(false)
var_dump("0" === false);     // bool(false)
var_dump("1" === true);      // bool(false)
var_dump(0 === "0");         // bool(false)

// !== 非严格不等
var_dump(0 !== "0");         // bool(true)

// <=> 太空船运算符(PHP 7.0+)
// 返回 -1(小于)、0(等于)、1(大于)
echo 1 <=> 2 . "\n";  // -1
echo 2 <=> 2 . "\n";  // 0
echo 3 <=> 2 . "\n";  // 1
echo "a" <=> "b" . "\n"; // -1

推荐使用严格比较

在条件判断中始终使用 ===!== 进行严格比较,避免因类型自动转换导致的意外行为。

实战示例

安全的类型检测与转换

php
<?php
declare(strict_types=1);

/**
 * 安全地获取整数类型的 GET 参数
 */
function getIntParam(string $name, int $default = 0, ?int $min = null, ?int $max = null): int
{
    if (!isset($_GET[$name])) {
        return $default;
    }

    // 使用 filter_var 进行严格验证
    $value = filter_var($_GET[$name], FILTER_VALIDATE_INT);

    if ($value === false) {
        return $default;
    }

    if ($min !== null && $value < $min) {
        return $min;
    }

    if ($max !== null && $value > $max) {
        return $max;
    }

    return $value;
}

// 使用
$page = getIntParam("page", 1, 1, 1000);
$limit = getIntParam("limit", 20, 1, 100);
echo "Page: {$page}, Limit: {$limit}\n";

表达式求值在模板中的应用

php
<?php
declare(strict_types=1);

// 模板变量处理:安全的表达式求值
class TemplateRenderer
{
    private array $data = [];

    public function assign(string $key, mixed $value): void
    {
        $this->data[$key] = $value;
    }

    public function render(string $template): string
    {
        extract($this->data, EXTR_SKIP);
        ob_start();
        include $template;
        return ob_get_clean();
    }

    public function escape(mixed $value): string
    {
        return htmlspecialchars((string)$value, ENT_QUOTES, "UTF-8");
    }
}

$renderer = new TemplateRenderer();
$renderer->assign("title", "用户管理");
$renderer->assign("count", 42);
$renderer->assign("users", [
    ["name" => "Alice", "active" => true],
    ["name" => "Bob", "active" => false],
]);

使用 match 表达式(PHP 8.0+)

match 表达式比传统的 switch 更严格和安全,使用 === 严格比较:

php
<?php
declare(strict_types=1);

function getHttpStatusText(int $code): string
{
    return match ($code) {
        200, 201, 204 => "Success",
        301, 302 => "Redirect",
        400 => "Bad Request",
        401 => "Unauthorized",
        403 => "Forbidden",
        404 => "Not Found",
        500 => "Internal Server Error",
        default => "Unknown Status",
    };
}

echo getHttpStatusText(200) . "\n"; // Success
echo getHttpStatusText(404) . "\n"; // Not Found
echo getHttpStatusText(999) . "\n"; // Unknown Status

// match 的严格比较特性
$value = "0";
$result = match (true) {
    $value === "0" => "string zero",
    $value === 0   => "integer zero",
    default         => "other",
};
echo $result . "\n"; // string zero

注意事项

1. PHP 8.0 字符串与数字比较的变更

PHP 8.0 改变了字符串与数字比较的行为。0 == "foo" 在 PHP 7 中为 true(字符串 "foo" 被转换为数字 0),在 PHP 8 中为 false

php
<?php
declare(strict_types=1);

// PHP 8.0+
var_dump(0 == "foo");   // bool(false)(PHP 7 为 true)
var_dump(0 === "foo");  // bool(false)(所有版本)

// 使用严格比较始终安全
var_dump(0 === "foo");  // bool(false)

2. null 与 falsy 值的混淆

php
<?php
declare(strict_types=1);

$value = "";
if (!$value) {
    echo "Value is empty\n";  // 会执行(空字符串是 falsy)
}

if (!isset($value)) {
    echo "Value is not set\n"; // 不会执行(变量已设置)
}

if ($value === null) {
    echo "Value is null\n";    // 不会执行(空字符串不是 null)
}

3. 隐式类型转换的风险

php
<?php
declare(strict_types=1);

// 危险:字符串数字比较
var_dump("10" == "1e1");    // bool(true)(科学计数法)
var_dump("10" === "1e1");   // bool(false)(严格比较)

// 危险:十六进制字符串
var_dump("0x10" == 16);     // bool(true)(PHP 7)
var_dump("0x10" === 16);    // bool(false)(严格比较)

// 安全:使用 filter_var
$number = filter_var("0x10", FILTER_VALIDATE_INT);
var_dump($number);          // bool(false)

最佳实践

  1. 始终使用严格比较:用 ===!== 替代 ==!=
  2. 使用 ?? 替代 isset() 检查:更简洁的 null 检查
  3. 区分 ???::前者检查 null,后者检查 falsy
  4. 使用 match 替代 switch:PHP 8.0+ 更安全的选择
  5. 使用 filter_var() 验证输入:比手动类型转换更安全
  6. 明确类型声明:配合 declare(strict_types=1) 减少隐式转换
  7. 括号消除歧义:复杂表达式中使用括号明确优先级

下一节

在掌握了表达式求值的规则之后,接下来将学习运算符的优先级——了解不同运算符之间的优先级关系和结合方向,是编写正确表达式的关键。请阅读 运算符优先级

参考链接