一、PHP数组不是“数组”,是有序哈希表
很多语言里,数组是一段连续内存,下标只能是整数。PHP不一样。PHP数组底层是哈希表(HashTable),同时维护了插入顺序。也就是说,它既能当索引数组用,也能当关联数组用,还能当有序映射用。
<?php
$arr = [
'name' => '代码号',
0 => '学习编程',
'php' => '教程',
];
print_r($arr);
// Array ( [name] => 代码号 [0] => 学习编程 [php] => 教程 )
键可以是整数,也可以是字符串。整数键和字符串键在底层会做归一化处理:"0"会被转成0,"1.5"保持字符串,true转成1,null转成空字符串""。这些规则不记住,调试时会很困惑。
个人经验:早期做一个表单接收功能,前端传id为字符串"0",后端用isset($data['id'])判断,结果因为键归一化,"0"和0是同一个键。当时排查了很久,后来养成习惯:接收外部数据后先明确键的类型,必要时用array_key_exists而不是isset,因为isset对null值返回false。
二、索引数组与关联数组:只是写法差异
<?php
// 索引数组
$langs = ['php', 'java', 'python'];
// 关联数组
$user = [
'name' => '代码号',
'lang' => 'php',
];
索引数组的键从0开始自动递增。如果手动指定了键,后续自动递增会从“较大整数键+1”继续。
<?php
$arr = [5 => 'a', 'b', 'c'];
print_r($arr);
// Array ( [5] => a [6] => b [7] => c )
这个细节在合并数组时容易出问题。array_merge会重新编号整数键,而+运算符保留原键,冲突时以左边为准。
<?php
$a = [1 => '代码号'];
$b = [1 => '学习编程', 2 => '教程'];
print_r($a + $b); // [1] => 代码号 [2] => 教程
print_r(array_merge($a, $b)); // [0] => 代码号 [1] => 学习编程 [2] => 教程
用哪个取决于业务:配置合并通常用array_merge_recursive或array_replace_recursive,而列表拼接用array_merge更自然。用+合并列表,第二个数组的整数键会被吞掉,这是常见坑。
三、遍历:foreach是主力,但别忽略细节
<?php
$user = ['name' => '代码号', 'lang' => 'php'];
foreach ($user as $key => $value) {
echo $key . ': ' . $value . PHP_EOL;
}
foreach操作的是数组的副本(写时复制),除非用引用。引用遍历的坑在上一条字符串引用里提过,这里再强调一次:
<?php
$nums = [1, 2, 3];
foreach ($nums as &$n) {
$n *= 2;
}
unset($n); // 必须断开
print_r($nums); // [2, 4, 6]
如果忘了unset($n),后面再用$n赋值,数组之后一个元素会被改掉。这个坑在循环嵌套时更隐蔽,外层引用没断,内层循环又用了同名变量,数据就乱了。
另一种遍历是array_walk、array_map、array_filter这类函数式写法。它们不修改原数组(array_walk可以改),适合做数据清洗。
<?php
$langs = ['php', 'java', 'python'];
$upper = array_map('strtoupper', $langs);
print_r($upper); // [PHP, JAVA, PYTHON]
个人建议:简单遍历用foreach,批量转换用array_map,条件筛选用array_filter。不要为了“看起来高级”而强行函数式,可读性优先。
四、常用函数:按场景分类记
-
增删改:
array_push、array_pop、array_shift、array_unshift、array_splice -
查找:
in_array、array_search、array_key_exists、isset -
排序:
sort、rsort、asort、ksort、usort -
运算:
array_diff、array_intersect、array_unique -
统计:
count、array_sum、array_count_values -
提取:
array_column、array_keys、array_values
其中array_column在项目里用得很多,比如从二维数组里取某一列:
<?php
$users = [
['id' => 1, 'name' => '代码号'],
['id' => 2, 'name' => '学习编程'],
];
$names = array_column($users, 'name');
print_r($names); // [代码号, 学习编程]
如果需要以id为键,可以传第三个参数:
<?php
$map = array_column($users, 'name', 'id');
print_r($map); // [1 => 代码号, 2 => 学习编程]
这个用法在“列表转映射”时省掉一个foreach,代码更短。
五、数组与引用:容易混淆的地方
数组变量赋值默认是值拷贝(写时复制)。但数组元素可以是引用:
<?php
$a = ['x' => '代码号'];
$b = &$a['x'];
$b = '学习编程';
print_r($a); // [x] => 学习编程
如果整个数组用引用赋值:
<?php
$a = ['x' => '代码号'];
$b = &$a;
$b['x'] = '学习编程';
print_r($a); // [x] => 学习编程
两种写法效果类似,但作用范围不同。前者只绑定一个元素,后者绑定整个数组。项目里做“配置共享”时,绑定整个数组更常见,但要注意:引用会跟着变量生命周期走,函数返回引用要格外小心。
六、性能取舍:大数组下的注意事项
PHP7以后数组性能提升明显,但仍有几个点值得注意:
-
in_array在大数组里是线性查找。如果数组有几千个元素,且需要频繁判断存在性,改用array_flip后isset,或者直接用键做映射。 -
array_merge在循环里调用会反复复制。合并多个数组时,先收集再一次性合并。 -
array_unique对大型数组开销不小。如果只是去重后做键值映射,用array_flip两次也能达到类似效果。 -
内存:PHP数组每个元素占用空间比C数组大,百万级数据要考虑生成器或SplFixedArray。
<?php
// 不推荐:循环内反复合并
$result = [];
foreach ($chunks as $chunk) {
$result = array_merge($result, $chunk);
}
// 推荐:先收集再一次合并
$result = array_merge(...$chunks);
项目反思:做过一个日志聚合功能,最初在循环里array_merge,数据量上万后明显变慢。改成先收集再合并,耗时下降了一半多。不是算法多高深,就是减少了重复复制。
七、项目实例:配置管理中的数组
<?php
$default = [
'db' => ['host' => 'localhost', 'port' => 3306],
'cache' => ['driver' => 'file'],
];
$env = [
'db' => ['host' => '192.168.1.10'],
'cache' => ['driver' => 'redis'],
];
$config = array_replace_recursive($default, $env);
print_r($config);
// db.host 被覆盖,db.port 保留,cache.driver 被覆盖
array_replace_recursive适合“默认配置+环境覆盖”。如果用array_merge_recursive,同键会合并成数组,反而不好用。这个区别在配置场景里很关键。
八、项目实例:数据分组
<?php
$orders = [
['user' => '代码号', 'amount' => 100],
['user' => '学习编程', 'amount' => 200],
['user' => '代码号', 'amount' => 50],
];
$grouped = [];
foreach ($orders as $order) {
$grouped[$order['user']][] = $order['amount'];
}
print_r($grouped);
// [代码号 => [100, 50], 学习编程 => [200]]
这种“按键分组”的写法在统计、报表、消息聚合里非常常见。也可以用array_reduce实现,但foreach更直观。
PHP数组的灵活来自哈希表底座,代价是某些操作比纯数组慢。理解键归一化、写时复制、引用残留、合并函数差异这几个点,能避开大部分日常坑。项目里用数组,先想清楚是“列表”还是“映射”,再选对应的函数和遍历方式。把unset、array_column、array_replace_recursive这几个工具用熟,代码会干净很多。