ZetCode

PHP 陷阱和边角情况

最后修改于 2025 年 4 月 2 日

本教程涵盖了常见的 PHP 陷阱和边角情况,这些可能会绊倒开发人员。

浮点数精度

由于精度限制,浮点数运算可能会产生意外的结果。

float_precision.php
<?php

$a = 0.1 + 0.2;
$b = 0.3;

echo $a == $b ? 'Equal' : 'Not equal';
echo "\n";
echo "Actual value: " . $a;

表达式 0.1 + 0.2 在浮点数中不完全等于 0.3。始终使用小的 epsilon 值比较浮点数,或使用 BC Math 函数进行精确计算。

字符串转数字转换

PHP 通过获取数字前缀将字符串转换为数字。

string_to_number.php
<?php

$str = "123abc";
$num = (int)$str;

echo $num; // 123
echo "\n";

$str = "abc123";
$num = (int)$str;

echo $num; // 0

如果不存在数字前缀,则返回 0。这种静默转换如果不正确处理,可能会导致错误。在转换之前,务必验证输入。

数组键转换

PHP 以意想不到的方式转换数组键:包含整数的字符串变为整数,浮点数被截断,布尔值变为 0 或 1。

array_key_casting.php
<?php

$arr = [
    "1" => "a",
    1 => "b",
    1.5 => "c",
    true => "d"
];

print_r($arr);

这可能导致意外的键冲突。请明确使用数组键以避免意外情况。

空字符串比较

在松散比较 (==) 中,PHP 认为 0"0"""false 相等。

strict_vs_loose.php
<?php

$var = 0;

if ($var == "") {
    echo "Equal\n";
} else {
    echo "Not equal\n";
}

// Strict comparison example
if ($var === "") {
    echo "Equal (Strict)\n";
} else {
    echo "Not equal (Strict)\n";
}

使用松散比较可能会导致意想不到的行为,例如在验证期间将 0false"" 视为等效。为了避免这些错误,请优先使用严格比较 (===),它会检查值和类型。

引用陷阱

PHP 对标量和数组的处理引用方式不同。

references.php
<?php

$a = 1;
$b = &$a;
$b = 2;

echo $a; // 2
echo "\n";

$array1 = [1, 2, 3];
$array2 = $array1;
$array2[0] = 5;

print_r($array1); // unchanged

分配数组会创建一个副本(写时复制),而分配引用 (&) 会创建一个别名。这种不一致性在使用引用时可能会引起混淆。

三元运算符优先级

三元运算符与字符串连接的优先级出乎意料。

ternary.php
<?php

$condition = true;
$result = $condition ? "true" : "false" . " concatenated";

echo $result; // "true concatenated" or "true"?

该表达式的计算结果为 ($condition ? "true" : "false") . " concatenated"。请始终使用括号来阐明复杂三元表达式中的意图。

可变变量

可变变量会使代码难以理解和维护。

variable_variables.php
<?php

$foo = "bar";
$$foo = "baz";

echo $bar; // "baz"
echo "\n";

// More complex example
$var = "hello";
$$var = "world";
$$$var = "universe";

echo $hello; // "world"
echo "\n";
echo $world; // "universe"

如果直接使用用户输入,它们还可能引入安全问题。尽可能避免使用它们,或者至少清楚地记录它们的使用情况。

数组合并 vs + 运算符

array_merge 和 + 运算符对数组的处理方式不同。

array_merge.php
<?php

$arr1 = ['a', 'b'];
$arr2 = ['c', 'd', 'e'];

$merged = array_merge($arr1, $arr2);
$added = $arr1 + $arr2;

print_r($merged);
print_r($added);

array_merge 附加值,而 + 保留键并且不会覆盖现有元素。这种差异很微妙,但在处理数字和字符串键时很重要。

递增/递减行为

PHP 的递增/递减运算符具有特殊行为。

increment.php
<?php

$a = 1;
echo $a++; // 1
echo "\n";
echo ++$a; // 3
echo "\n";

$b = "abc";
$b++;
echo $b; // "abd"

后递增返回递增前的值。前递增返回新值。此外,字符串递增遵循 Perl 规则(“z”变成“aa”)。了解这些边缘情况以避免意外情况。

switch 语句的类型转换

switch 语句使用松散比较 (==),这可能导致意外匹配。

switch.php
<?php

$var = "0";

switch ($var) {
    case 0:
        echo "Zero\n";
        break;
    case "0":
        echo "String Zero\n";
        break;
    default:
        echo "Other\n";
}

在此示例中,两个 case 都会匹配字符串 "0"。当类型很重要时,请使用 if-elseif 的严格比较 (===),或者注意这种行为。

函数作用域

默认情况下,PHP 函数无法访问父作用域中的变量。

scope.php
<?php

$global = "global";

function test() {
    echo $global; // Undefined variable
    echo "\n";
    
    global $global;
    echo $global; // Now works
}

test();

必须使用 global 关键字或 $GLOBALS 数组。这与其他许多语言不同,并且可能会引起混淆。考虑将变量作为参数传递,而不是使用 global。

空构造

empty 认为 0、"0"、""、null、false 和空数组为空。

empty.php
<?php

$var = "0";

if (empty($var)) {
    echo "Empty\n";
} else {
    echo "Not empty\n";
}

这很有用,但可能会令人惊讶。isset 检查变量是否存在且不为 null。了解 emptyissetis_null 之间的区别。

按值分配数组

数组按值分配(写时复制),而对象按引用分配。

array_copy.php
<?php

$arr1 = ['a' => 1, 'b' => 2];
$arr2 = $arr1;
$arr2['a'] = 3;

print_r($arr1); // unchanged
print_r($arr2); // changed

// But with objects:
class Obj {}
$obj1 = new Obj();
$obj1->prop = 1;
$obj2 = $obj1;
$obj2->prop = 2;

echo $obj1->prop; // 2

如果不理解这种不一致性,可能会导致错误。当您需要副本时,请对对象使用 clone,或者注意这种行为。

错误抑制

@ 运算符会抑制错误,但这会使调试变得困难,并且会产生性能开销。

error_suppression.php
<?php

@$value = 1/0; // Division by zero suppressed
echo "Script continues\n";

// Better approach:
set_error_handler(function($errno, $errstr) {
    echo "Error handled: $errstr\n";
    return true;
});

$value = 1/0; // Triggers custom handler
echo "Script continues\n";

相反,请使用 try/catch 进行异常的正确错误处理,或使用 set_error_handler() 进行传统错误处理。在开发期间使错误可见。

循环中的变量作用域

循环闭包中的变量捕获最终值,而不是迭代时间的值。

loop_scope.php
<?php

$funcs = [];
for ($i = 0; $i < 3; $i++) {
    $funcs[] = function() use ($i) {
        return $i;
    };
}

foreach ($funcs as $f) {
    echo $f() . "\n";
}

// Fixed version:
$funcs = [];
for ($i = 0; $i < 3; $i++) {
    $x = $i;
    $funcs[] = function() use ($x) {
        return $x;
    };
}

这是在循环中创建闭包时常见的陷阱。解决方案是将循环变量复制到循环内的临时变量中。

来源

PHP 语言参考

本教程涵盖了开发人员应该了解的常见 PHP 陷阱和边角情况。

作者

我叫 Jan Bodnar,是一位充满激情的程序员,拥有丰富的编程经验。自 2007 年以来,我一直在撰写编程文章。迄今为止,我撰写了 1,400 多篇文章和 8 本电子书。我拥有超过十年的编程教学经验。

列出所有 PHP 教程。