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 库。

坑清单

  1. array_filter 之后没 array_values,json_encode 出来是对象。
  2. + 与 array_merge 的数字键行为不同。
  3. $arr[100] = x 之后 $arr[] 的键是 101。
  4. "08" 作为键不会转成 int,但 "8" 会。
  5. in_array 不加 true,"1" 能匹配到 1。
  6. array_shift/array_unshift 在循环里用,O(n²)。
  7. 数组里的对象在赋值时不复制。
  8. foreach 用引用后没 unset。