PHP 基本语法 04|数组:一个结构同时装下 list 和 map
它不是"数组",是有序哈希表
PHP 的数组和 C 的数组、Java 的 List 都不是一回事。它同时承担了三种角色:列表(list)、字典(map)、以及有序集合。底层是哈希表 + 双向链表,所以既能按下标访问,又能保持插入顺序。
<?php
$list = [10, 20, 30]; // 索引数组,键自动是 0,1,2
$map = ['name' => '张三', 'age' => 28]; // 关联数组
// 混着来也合法
$mixed = [0 => 'a', 'k' => 'b', 5 => 'c'];
这个设计让 PHP 处理"JSON 风格的数据"极其顺手,代价是内存占用比真正的 C 数组大得多(每个元素都是一个 zval 加一个 bucket)。一个亿级元素的数组在 PHP 里基本不可行,得换 SplFixedArray 或者直接交给数据库/其他服务。
定义与访问
<?php
// 两种语法,[] 是新写法
$old = array(1, 2, 3);
$new = [1, 2, 3];
// 追加:取当前最大整数键 + 1
$arr = [];
$arr[] = 'a'; // 键 0
$arr[] = 'b'; // 键 1
$arr[10] = 'c'; // 键 10
$arr[] = 'd'; // 键 11(不是 2)
最后这一条很关键:$arr[] 追加的键是"当前最大整数键 + 1",不是元素个数。很多人在 $arr[100] = x 之后 $arr[] = y,结果索引跳到了 101,for ($i=0; $i<count($arr); $i++) 就取不到东西。
访问不存在的键会报 Warning(PHP 8 起是 Warning,不再是 Notice),并且返回 null:
<?php
$arr = ['a' => 1];
echo $arr['b'] ?? '默认'; // ✅ 用 ?? 兜底
echo $arr['b']; // ⚠️ Warning: Undefined array key "b"
键名的类型转换规则
这一块几乎每个人都会被坑一次:
<?php
$arr = [
"8" => 'A', // "8" 是十进制整数字符串 → 转成 int(8)
8.7 => 'B', // float 截断 → int(8),覆盖上面的值!
true => 'C', // 转成 int(1)
false => 'D', // 转成 int(0)
null => 'E', // 转成 ""(空字符串键)
"08" => 'F', // "08" 不是合法十进制整数 → 保留字符串键
"1.5" => 'G', // 保留字符串键
];
var_dump($arr);
总结成一句话:纯十进制整数字符串会自动变 int,带前导零或小数点的不变。
<?php
var_dump(["8" => 'x']); // [8 => 'x'] 键是 int
var_dump(["08" => 'x']); // ["08" => 'x'] 键是 string
这就是为什么 array_keys() 返回的键类型有时是 int 有时是 string。用 array_keys() 之后做严格比较(in_array($k, $keys, true))会踩这个坑。
解构赋值
<?php
// 列表解构
[$a, $b, $c] = [1, 2, 3];
// 跳过某些位置
[, , $third] = ['a', 'b', 'c'];
// 关联解构(PHP 7.1+)
['name' => $n, 'age' => $age] = ['name' => '张三', 'age' => 28];
// 嵌套
[[$x, $y], [$z, $w]] = [[1, 2], [3, 4]];
// 交换两个变量,不用临时变量
[$a, $b] = [$b, $a];
list() 是同一件事的旧写法,新代码用 [] 就行。
遍历
<?php
$users = [
['name' => '张三', 'age' => 28],
['name' => '李四', 'age' => 32],
];
foreach ($users as $u) {
echo $u['name'], PHP_EOL;
}
foreach ($users as $i => $u) {
echo "$i: {$u['name']}", PHP_EOL;
}
// 遍历时解构(PHP 7.1+)
foreach ($users as ['name' => $name, 'age' => $age]) {
echo "$name $age", PHP_EOL;
}
foreach 遍历的是数组的副本(延迟拷贝,写时才真复制),所以在循环里修改原数组不会影响当前遍历的元素:
<?php
$a = [1, 2, 3];
foreach ($a as $v) {
$a[] = $v * 2; // 原数组变了,但遍历范围仍是初始的三个
}
print_r($a); // [1,2,3,2,4,6]
要边遍历边改,用引用:
<?php
$a = [1, 2, 3];
foreach ($a as &$v) {
$v *= 2;
}
unset($v); // ⚠️ 一定要解除,否则后面复用 $v 会出事
数组赋值是"拷贝",对象赋值是"引用"
这条是 PHP 数组与对象最本质的区别:
<?php
$a = ['x' => 1];
$b = $a;
$b['x'] = 2;
echo $a['x']; // 1 —— 数组拷贝
$o1 = new stdClass();
$o1->x = 1;
$o2 = $o1;
$o2->x = 2;
echo $o1->x; // 2 —— 对象是同一个实例
PHP 用写时复制(COW)优化:$b = $a 时并不真复制,直到有一方被修改。所以"大量拷贝小数组"实际开销比想象的小,但大数组真的被改一次,就会整块复制——这是内存暴涨的常见原因。
想按引用传数组,显式加 &:
<?php
function addItem(array &$list, $item) {
$list[] = $item; // 直接改原数组,不复制
}
合并:+ 和 array_merge 不是一回事
<?php
$a = ['x' => 1, 'y' => 2];
$b = ['y' => 3, 'z' => 4];
print_r($a + $b); // ['x'=>1, 'y'=>2, 'z'=>4] —— 左边优先,不覆盖
print_r(array_merge($a, $b));// ['x'=>1, 'y'=>3, 'z'=>4] —— 右边覆盖
还有一条更隐蔽的差异:array_merge 对数字键会重新编号,+ 不会。
<?php
$a = [1, 2];
$b = [3, 4];
print_r($a + $b); // [0=>1, 1=>2] —— 索引 0,1 被占,$b 全丢
print_r(array_merge($a, $b)); // [1,2,3,4] —— 数字键重排,正确合并
做列表合并用 array_merge,做配置数组"不覆盖已有值"的合并用 +。这个区别搞错,会出现"新加的元素莫名其妙消失"。
PHP 8.1 起 array_merge 支持字符串键的展开运算符:
<?php
$c = [...$a, ...$b]; // 8.1+ 字符串键也可以展开,行为同 array_merge
常用的二十来个函数
增删
<?php
$arr = [1, 2, 3];
array_push($arr, 4, 5); // 末尾加多个,等价 $arr[] = 4; $arr[] = 5;
array_pop($arr); // 弹出末尾
array_unshift($arr, 0); // 头部加(注意:所有元素键都要重排,慢)
array_shift($arr); // 弹出头部(同样慢)
array_splice($arr, 1, 2); // 从下标 1 删 2 个,后面的补位
array_splice($arr, 1, 0, ['x', 'y']); // 在 1 处插入
array_shift/array_unshift 是 O(n) 操作,在循环里用它们处理大数组会非常慢。改成"用队列下标推进"或直接用 SplQueue。
查找与过滤
<?php
$arr = ['a' => 1, 'b' => 2, 'c' => 3];
var_dump(in_array(2, $arr)); // true
var_dump(in_array("2", $arr, true)); // false —— 严格模式必需
var_dump(array_search(2, $arr)); // "b" —— 返回值,找不到返回 false
var_dump(array_key_exists('a', $arr)); // true,null 值也能判定存在
$filtered = array_filter($arr, fn($v) => $v > 1);
$byKey = array_filter($arr, fn($k) => $k !== 'a', ARRAY_FILTER_USE_KEY);
$both = array_filter($arr, fn($v, $k) => $v > 1 && $k !== 'c', ARRAY_FILTER_USE_BOTH);
array_filter 会保留原来的键。过滤后如果要输出 JSON 数组,必须 array_values():
<?php
$nums = [1, 2, 3, 4, 5];
$even = array_filter($nums, fn($n) => $n % 2 === 0);
echo json_encode($even); // {"1":2,"2":4} ← 不是数组!
echo json_encode(array_values($even)); // [2,4] ← 正确
这个问题导致的 bug 极其常见(前端收到对象而非数组,遍历报错)。
转换与聚合
<?php
$rows = [
['id' => 1, 'name' => '张三', 'dept' => '技术'],
['id' => 2, 'name' => '李四', 'dept' => '技术'],
['id' => 3, 'name' => '王五', 'dept' => '市场'],
];
// 取某一列(做下拉框、批量 ID 最常用)
print_r(array_column($rows, 'name', 'id')); // [1=>'张三', 2=>'李四', 3=>'王五']
// 按某列去重
print_r(array_unique(array_column($rows, 'dept')));
// 求和
echo array_sum([1, 2, 3]); // 6
// 键值翻转(值必须能当键)
print_r(array_flip(['a' => 'x', 'b' => 'y'])); // ['x'=>'a', 'y'=>'b']
// 生成器式的范围
print_r(range(1, 5)); // [1,2,3,4,5]
print_r(range('a', 'e')); // ['a','b','c','d','e']
// 数值列表判定(PHP 8.1)
var_dump(array_is_list([1, 2, 3])); // true
var_dump(array_is_list([1 => 'a'])); // false
array_is_list() 在判断"要不要用 array_values 修一下"时特别有用:
<?php
function toJson(array $a): string {
return json_encode(array_is_list($a) ? $a : array_values($a));
}
映射与归约
<?php
$ids = [1, 2, 3];
$double = array_map(fn($x) => $x * 2, $ids);
$total = array_reduce($ids, fn($carry, $x) => $carry + $x, 0);
// array_map 支持多数组,按位置传参
$sums = array_map(fn($a, $b) => $a + $b, [1,2], [10,20]); // [11, 22]
array_map 传 null 作为回调可以"合并多个数组":
<?php
$names = ['张三', '李四'];
$ages = [28, 32];
print_r(array_map(null, $names, $ages)); // [['张三',28], ['李四',32]]
排序
<?php
$arr = [3, 1, 2];
sort($arr); // 按值升序,键重排
rsort($arr); // 降序
asort($arr); // 按值升序,保留键
ksort($arr); // 按键升序
usort($arr, fn($a, $b) => $a <=> $b); // 自定义比较
uasort($arr, fn($a, $b) => $a <=> $b); // 自定义 + 保留键
多字段排序的写法:
<?php
usort($rows, function ($a, $b) {
return ($a['dept'] <=> $b['dept'])
?: ($b['id'] <=> $a['id']); // 部门升序,同部门 id 降序
});
?: 在这里很妙:第一个比较结果为 0(相等)时才会看第二个。
PHP 8.4 的新函数
如果你的项目已经升到 8.4,可以少写一堆 array_filter 加 reset:
<?php
$nums = [1, 2, 3, 4];
array_find($nums, fn($n) => $n > 2); // 3 找不到返回 null
array_find_key($nums, fn($n) => $n > 2); // 2
array_any($nums, fn($n) => $n > 3); // true
array_all($nums, fn($n) => $n > 0); // true
这是这些年 PHP 数组 API 最实用的一次补充,直接对应 JS 的 find/some/every。
深拷贝:数组里的对象不会跟着复制
<?php
$obj = new stdClass();
$obj->name = '原始';
$a = ['item' => $obj];
$b = $a; // 数组被"拷贝"了
$b['item']->name = '改过的';
echo $a['item']->name; // 改过的 —— 对象的引用还共享
需要真正独立,用 clone,或者 unserialize(serialize($x))(深拷贝但性能差、且会触发 __wakeup),或者用专门的 deep clone 库。
坑清单
array_filter之后没array_values,json_encode出来是对象。+与array_merge的数字键行为不同。$arr[100] = x之后$arr[]的键是 101。"08"作为键不会转成 int,但"8"会。in_array不加true,"1"能匹配到1。array_shift/array_unshift在循环里用,O(n²)。- 数组里的对象在赋值时不复制。
foreach用引用后没unset。