PHP 的数组函数太多,有几个我用了很久才发现理解错了。记一下。
一、array_map 的参数顺序和别的函数反着来
// 大部分数组函数是:array_xxx(数组, ...)
array_filter($arr, $callback);
array_reduce($arr, $callback);
array_walk($arr, $callback);
// 但 array_map 是:回调在前
array_map($callback, $arr);
每次写都要愣一下。这个不一致没什么好办法,记住就行。
更值得注意的是 array_map 传多个数组时的行为:
$a = [1, 2, 3, 4];
$b = ['a', 'b'];
$result = array_map(fn($x, $y) => "$x$y", $a, $b);
// ['1a', '2b', null, null] —— 不对,实际是报错还是补 null?
实际结果是 ['1a', '2b', '3', '4']。长度按最长的数组算,短的用 null 补。回调收到 $y = null。
如果回调声明了 string $y,就会 TypeError。所以多数组 map 之前最好先确认长度。
二、array_filter 保留原键,array_values 经常要跟着
$nums = [1, 2, 3, 4, 5];
$even = array_filter($nums, fn($n) => $n % 2 === 0);
print_r($even);
// [1 => 2, 3 => 4]
键保留了。如果之后要 json_encode,会变成对象 {"1":2,"3":4} 而不是数组 [2,4]。前端拿到这种东西会懵。
$even = array_values(array_filter($nums, fn($n) => $n % 2 === 0));
// [2, 4]
我现在只要 filter 完还要继续用(尤其是要输出 JSON),就顺手加 array_values。
同样的坑还有 unset($arr[0])——删了元素但键不重排,转 JSON 还是对象。
三、in_array 默认松散比较
这个坑我踩过,而且是线上事故。
$allowed = ['active', 'pending'];
if (in_array($status, $allowed)) {
// 放行
}
如果 $status 是 0(int),in_array(0, ['active', 'pending']) 在松散模式下……
PHP 8 之前:'active' == 0 为 true(字符串转数字是 0),所以返回 true。未授权通过了。
PHP 8 之后这个具体例子改了(字符串和数字比较的规则变了),但松散比较的风险还在:
in_array('1abc', [1, 2, 3]) // PHP 8 下是 false,PHP 7 下是 true
in_array(0, ['a', 'b']) // PHP 8 下 false,PHP 7 下 true
in_array(null, ['a']) // 松散比较 null == 'a' 是 false
in_array(false, ['a']) // false == 'a' 为 false,但 false == '' 为 true
解决方法:永远用 in_array($needle, $haystack, true)。
第三个参数 true 开启严格模式。加上之后类型不同直接 false。
我现在所有 in_array 都带 true,代码检查工具也配了这个规则。
顺便:array_search 也有同样的问题,也要传 true。
四、array_merge 和 + 的区别
$a = ['x' => 1, 'y' => 2];
$b = ['y' => 3, 'z' => 4];
// array_merge:后面的覆盖前面
array_merge($a, $b); // ['x'=>1, 'y'=>3, 'z'=>4]
// + :前面的优先,不覆盖
$a + $b; // ['x'=>1, 'y'=>2, 'z'=>4]
数字键的行为完全不同:
$a = [1, 2];
$b = [3, 4];
array_merge($a, $b); // [1, 2, 3, 4] 重新编号
$a + $b; // [1, 2, 4] 键 0 和 1 保留前面的,$b 的 3 被丢掉
+ 对数字键是"按位置合并",array_merge 是"追加"。我见过有人想合并两个列表用了 +,结果丢了一半数据。
还有个相关的:array_merge 遇到 null 会警告(PHP 8 里是 TypeError),而 + 不会:
array_merge($a, null); // PHP 8: TypeError
$a + null; // TypeError 也会
实际上两个都不接受 null。真正要合并可能为空的数组,用:
array_merge($a, $b ?? []);
PHP 8.1 加了 array_merge(...$arrays) 传空数组时返回 [],比 8.0 合理。
五、sizeof 是 count 的别名,但别用
sizeof 和 count 完全一样。但 sizeof 在 C 里是编译期的字节大小,语义完全不同。用 count 更清楚。
另外 count(null):
- PHP 7.x:返回 0,带警告
- PHP 8.0+:TypeError
所以 count($maybeNull) 从 8.0 开始会直接报错,升级的时候是个常见破坏点。
六、foreach 里的引用陷阱
$arr = [1, 2, 3];
foreach ($arr as &$v) {
$v *= 2;
}
// $arr 现在是 [2, 4, 6]
// 但 $v 还是引用,指向 $arr[2]
foreach ($arr as $v) { // 忘了 unset($v)
// 这个循环会破坏数组!
}
第二个 foreach 会出问题,因为 $v 还是引用。结果是 [2, 4, 4] 而不是 [2,4,6] 的副本。
习惯:引用循环之后立即 unset($v)。
foreach ($arr as &$v) {
$v *= 2;
}
unset($v);
我现在基本不用引用循环了,改用索引或者 array_map:
$arr = array_map(fn($v) => $v * 2, $arr);
七、array_unique 的比较规则
array_unique([1, '1', true]);
// [1] —— 全都算重复
默认是字符串比较(SORT_STRING),所以 1、'1'、true 都变成 '1' 后相等。
要严格比较得自己写:
$unique = [];
foreach ($arr as $v) {
$key = gettype($v) . ':' . $v;
$unique[$key] = $v;
}
$unique = array_values($unique);
array_unique 也保留原键,别忘了 array_values。
八、array_column 处理对象
$users = [
['id' => 1, 'name' => 'A'],
['id' => 2, 'name' => 'B'],
];
array_column($users, 'name', 'id'); // [1 => 'A', 2 => 'B']
对象也行(PHP 7.0+):
$users = [$u1, $u2]; // stdClass 或任何有 public 属性的对象
array_column($users, 'name', 'id');
但私有属性取不到。如果是 Doctrine 实体,得先转数组。
还有个限制:array_column 只能取一层。嵌套的 ['a' => ['b' => 1]] 不能直接取 a.b,得自己 map。
九、compact 和 extract:少用
compact 根据变量名生成数组:
$name = 'x';
$age = 1;
compact('name', 'age'); // ['name' => 'x', 'age' => 1]
看着简洁,但重构的时候很危险——改个变量名,字符串还是老的,运行时才报 notice。
extract 更危险,会把数组键变成变量,覆盖已有的:
$data = ['config' => 'evil'];
extract($data);
// 本地的 $config 被覆盖了
我在项目里禁用了这两个,用显式数组赋值代替,多打几个字但安全。
十、一个性能相关的
// 循环里 count
for ($i = 0; $i < count($arr); $i++) { } // 每次都调 count
// 缓存长度
$n = count($arr);
for ($i = 0; $i < $n; $i++) { }
PHP 7 之后 count 是 O(1)(数组结构里存了长度),所以这个优化基本没意义了。但如果是 Countable 对象,count() 会调方法,那就有开销。
还有 isset($arr[$k]) > 0 这种写法,不如直接 !empty($arr[$k])。不过 empty 会把 0、'0'、false 都当空,语义不同要小心。
真正该注意的是在循环里做数组查找:
// O(n²)
foreach ($a as $x) {
if (in_array($x, $b)) { } // 每次扫一遍 $b
}
// O(n):先把 $b 转成键
$bSet = array_flip($b);
foreach ($a as $x) {
if (isset($bSet[$x])) { } // O(1)
}
这个优化在数据量大时差别非常大。我们有个接口从 1.2 秒降到 40ms,就是改了这个。