
聊到 PHP 数组很多人第一反应是“不就是个存数据的容器嘛”但真正写出高质量 PHP 代码的人几乎都在数组上花过大力气。数组在 PHP 里不只是列表它同时承担了字典、集合、队列、栈等多种角色甚至可以说PHP 的数组设计决定了整个语言的开发体验。这章“PHP 基础案例教程之 04-数组”就是要把这块硬骨头啃透从底层逻辑讲起到增删改查、遍历排序、二维数组、去重合并再到面试里最常考的数组陷阱一条线捋清楚。这篇内容适合谁刚学完 PHP 语法、想在数组上系统进阶的新手也适合写过一段时间 PHP、但总在数组操作上“凭感觉写”的开发者。我尽量用实际项目里的场景说话讲清楚每个函数背后的设计意图以及哪些坑是文档里不会写的。跟着走一遍你会发现数组不再是“背函数名”而是一套可以顺手组合的工具箱。1. 先搞懂 PHP 数组的底层设计再谈操作1.1 PHP 数组不是传统数组而是“有序映射”接触过 C 语言或 Java 的人习惯把数组理解为“连续内存空间里按索引存放的一组元素”。PHP 的数组完全不是这个模型。PHP 数组本质上是有序哈希表HashTable也叫有序映射。它同时维护了两样东西一个哈希索引结构用于根据键快速定位值一个双向链表用于记录元素插入的顺序。这样带来的直接好处是PHP 数组既可以按下标访问如$arr[0]又可以用字符串当键如$user[name]而且遍历顺序就是插入顺序不需要像普通哈希表那样担心顺序被打乱。这个设计让 PHP 在 Web 开发场景里极其舒服——从数据库查出来一行记录直接就是一个关联数组字段名就是键根本不需要额外映射。换个角度理解PHP 数组像你手里的一个笔记本既能按页码数字下标翻也能按标签字符串键找而且你记录内容的先后顺序永远保留着。这个特性是 PHP 数组所有操作的基础记住它后面很多函数的行为了然于心。1.2 数字键与字符串键的自动转换规则PHP 数组的键有两种形态integer和string。当使用字符串键时有一个坑必须提前知道包含合法十进制整数的字符串键会被自动转换为整数键。$arr [ 1 a, // 键 1 会被转为整数 1 1 b, // 与上面冲突覆盖为 b 01 c, // 01 不是合法十进制整数格式保持字符串键 1.5 d, // 浮点格式保持字符串键 ]; print_r($arr); // 输出Array ( [1] b [01] c [1.5] d )这个规则在接收前端参数、读取配置文件时会“暗算”你。比如前端传过来一个 JSON 对象键是1用json_decode($json, true)转成数组后键已经悄悄变成了整数1。如果你再用isset($arr[1])判断其实也能命中因为 PHP 比较键时会自动转换但如果你用array_key_exists(1, $arr)配合严格模式就要小心了。我自己的习惯是凡是从外部输入进入的键统一用(string)强制转换后再做判断或者直接用array_key_exists()配合原始数据避免依赖 PHP 的隐式转换逻辑。1.3 数组 vs 对象什么时候用哪个热词里有一条是“C# 不同的 class 可以组成数组吗”放到 PHP 语境下答案很明确可以。PHP 数组可以存放任意类型包括对象甚至一个数组里可以混着数组和对象。但实际开发中要思考的是什么时候用数组什么时候用对象数据是“无行为的纯值集合”比如配置项、列表数据→ 用数组操作函数多轻量高效。数据带有行为有方法、有规则→ 定义类用对象。与外部系统交互如 API 的 JSON→ 数组更方便序列化、遍历、转换都不需要额外写 getter/setter。常见误区是“万物皆数组”。看到一个用户的完整信息有人喜欢这样写$user [ name 张三, age 25, getIntroduce function () { ... }, ];这种写法极其不推荐。把闭包塞进数组里虽然语法上允许但会让调试、序列化、缓存全部变得困难。数组承载数据对象承载行为各司其职这个边界要清晰。2. 数组的定义、遍历和指针操作把这些基础盘扎实2.1 定义数组的两种方式和初始化细节PHP 里定义数组有两种语法老代码里是array()PHP 5.4 之后推荐用[]。推荐用[]不只是因为短更重要的是它和 JSON 的视觉结构一致阅读起来更直观。// 方式一传统写法 $colors array(red, green, blue); // 方式二短数组语法推荐 $colors [red, green, blue]; // 关联数组 $config [ host 127.0.0.1, port 3306, debug true, ];初始化时有几个细节要养成习惯不指定键时PHP 自动使用“当前最大整数键 1”作为新键。注意这里的关键是“当前最大整数键 1”不是“元素个数 1”。如果数组里已有键5和键100追加一个新元素它的键是101不是3。显式指定键时如果键重复后面的值覆盖前面的值。尾随逗号是合法的PHP 5.6 起多行数组建议最后留一个逗号方便后续增删行减少 diff 噪音。$items [ a 1, b 2, // 这个尾随逗号合法且推荐 ];2.2 遍历数组foreach、for、while 各有适用场景遍历是数组操作里最核心的环节。foreach是首选它同时支持值和键且不会像for那样受数组内部指针影响。$users [ [name Alice, age 24], [name Bob, age 30], ]; foreach ($users as $index $user) { echo $index . : . $user[name] . PHP_EOL; }for循环适合纯数字索引数组但要注意如果数组中间有空洞例如unset($arr[1])之后for ($i 0; $i count($arr); $i)会漏掉元素或访问到不存在的键。这是最常见的一个 bug 来源。所以遍历大数组时我更推荐foreach它不关心键是否连续。while each()已经是老古董了PHP 8 里each()被彻底移除别再用了。array_walk适合需要对每个元素做统一处理并且需要修改值的场景配合引用传递$numbers [1, 2, 3, 4]; array_walk($numbers, function ($value, $key) { $value * 2; }); unset($value); // 解除引用防止后面误改2.3 数组指针内部指针移动与循环队列的类比热词里有一条与数据结构相关“假设以数组 q[m] 存放循环队列中的元素同时以 rear 和 length 分别指示环形队列中的队尾和长度”。这是 C 语言数据结构课的经典题但在 PHP 里数组自带“内部指针”概念虽然日常写业务代码用得少但了解它有助于理解一些函数的实现原理。PHP 数组内部有一个指针指向当前元素。current()获取当前元素值key()获取当前键next()和prev()移动指针reset()把指针拨回开头end()把指针拨到末尾。$arr [a, b, c]; echo current($arr); // a next($arr); echo current($arr); // b reset($arr); echo current($arr); // a基于这些函数可以轻松用 PHP 数组实现一个循环队列的遍历逻辑function circularTraverse(array $arr, $startIndex, $count) { if (empty($arr)) return; reset($arr); // 先把指针移动到起始位置 for ($i 0; $i $startIndex; $i) { next($arr); } for ($i 0; $i $count; $i) { $item current($arr); if ($item false) { reset($arr); // 模拟环形回绕 $item current($arr); } echo $item . ; next($arr); } echo PHP_EOL; }不过说实话实际业务代码里直接用指针操作的场景很少。foreach才是主流遍历方式。指针相关的函数主要用在一些特殊场景比如从一个大数组里按顺序取一段数据、实现简易的分页器之类。注意foreach操作的是数组的副本写时复制不会改变数组内部指针位置。如果你在foreach之前调用了next()移动指针foreach还是会从数组开头遍历。只有通过引用遍历时例外。3. 数组的核心操作详解每一个都很常用3.1 增删改查的常规操作与性能考量增删改查是数组的日常。这里值得展开几个容易被忽略的点。增加元素追加$arr[] $value;PHP 自动分配键性能高。指定键添加$arr[key] $value;如果键已存在就是修改。删除元素unset($arr[key])删除指定键。删完后不会重新索引数字键数组会留下“空洞”。array_splice($arr, $offset, $length)删除一段并重新索引适合需要连续键的场景。array_pop()删除最后一个元素array_shift()删除第一个元素并重新索引所有数字键。注意array_shift()在大数组上性能较差因为它要重新索引时间复杂度 O(n)。如果频繁操作头部元素考虑用SplDoublyLinkedList代替。查找元素in_array($value, $arr)判断值是否存在非严格模式比较。array_search($value, $arr)返回键找不到返回false。注意false和0的区分要用判断。array_key_exists($key, $arr)判断键是否存在。它只查键不查值比isset()更准确因为isset()在值为null时返回false而array_key_exists()返回true。这一点值得反复强调isset()和array_key_exists()的行为差异是面试高频题。$arr [a null]; var_dump(isset($arr[a])); // false var_dump(array_key_exists(a, $arr)); // true做表单校验、接口参数判断时如果你用isset()来判断用户传了某个字段但值为null会漏判。这时候应该用array_key_exists()。3.2 数组合并、相加与递归合并的区别数组合并是日常开发里很容易踩坑的操作。array_merge()和运算符的结果完全不同。$base [a 1, b 2]; $override [b 20, c 30]; $result1 array_merge($base, $override); // [a 1, b 20, c 30] 后面的覆盖前面的同名键 $result2 $base $override; // [a 1, b 2, c 30] 保留前面的键后面的同名键被忽略array_merge()字符串键同名时后者覆盖前者数字键不会覆盖而是追加并重新索引。运算符只要键已存在无论键的类型前面的数组优先后者不覆盖。array_merge_recursive()不会覆盖同名键而是把同名键的值合并成数组。它常用于合并配置但要注意它递归合并数组如果你预期的是“覆盖”用array_replace_recursive()更合适。实际项目中做配置合并、多级数组合并时我一般用array_replace_recursive()它的语义更符合直觉后面的覆盖前面的同名键但只覆盖存在的路径不会像array_merge_recursive()那样意外地造出嵌套数组。3.3 数组排序全家桶以及 “稳定排序” 的坑排序是数组操作里最需要耐心的一part。PHP 的排序函数非常多不下十几个但核心需要记住一组对应关系作用按值排序升序按值排序降序按键排序升序按键排序降序保持键值关联asort()arsort()ksort()krsort()不保持键值关联sort()rsort()ksort()krsort()sort()排序后索引会重置为 0、1、2...键值关联消失。asort()保留原始键适合对关联数组按值排序。usort()/uasort()/uksort()可以用自定义函数比较元素适合复杂排序。排序函数都是直接修改原数组并返回true/false不是返回新数组。这是新手最容易搞错的地方。还有一个隐蔽的坑PHP 的排序不是稳定排序。PHP 8.0 之前sort()系列在元素“相等”时不保证保留原来的相对顺序。如果你的业务对排序稳定性有要求比如先按时间排序相同时间的条目要按原顺序展示需要在自定义比较函数里加一个“次要键”来打破平局usort($records, function ($a, $b) { if ($a[time] $b[time]) { return $a[id] $b[id]; // 用 id 打破平局 } return $a[time] $b[time]; });3.4 数组去重的正确姿势热词里出现了“数组去重”这个词。array_unique()是最直接的答案但它有几个细节去重时用string比较规则1和1会被认为相同这通常是期望行为。它保留第一次出现的键。例如$arr [a 1, b 1]去重后保留a 1。对于大数组array_unique()性能尚可但如果数组元素是海量字符串可以考虑先array_flip()再array_flip()$unique array_flip(array_flip($arr)); // array_flip 只适用于值可以当作键用的数组整数或字符串不过这一步有可能改变键的顺序翻转两次后键会变成整数键所以业务里还是要看需求。如果只是去掉重复值且不关心键array_values(array_unique($arr))就是最简单的清理方式。3.5 数组转字符串的各种姿势数组转字符串是接口对接、日志记录、SQL 拼装里的高频操作。依据场景不同工具也不同简单拼接implode(,, $arr)。注意如果数组值是数组implode会抛 Warning需要先递归展平或手动处理。递归转字符串用print_r($arr, true)或var_export($arr, true)这两者都返回字符串而不是直接输出适合调试日志。转 JSONjson_encode($arr)。这是现代 PHP 里最常见的方式。要注意json_encode对中文默认是转成\uXXXX格式的如果希望输出中文用JSON_UNESCAPED_UNICODE标志$data [name 张三]; echo json_encode($data, JSON_UNESCAPED_UNICODE); // 输出{name:张三} 而不是 {name:\u5f20\u4e09}如果用serialize()转字符串它是 PHP 原生格式可完整还原类型包括null、布尔、对象等适合缓存存储。但serialize()的结果不适合直接给外部系统读写它是 PHP 私有的。4. 二维数组与复杂数据结构操作4.1 二维数组的遍历与转换场景二维数组是 Web 开发里最常接触的数据结构。数据库查询结果集就是一个二维数组外层每行一条记录内层是字段名和字段值的映射。$rows [ [id 1, name Alice, score 90], [id 2, name Bob, score 85], [id 3, name Cathy, score 95], ];常见需求是“以某个字段作为外层键”$indexed array_column($rows, null, id); // 结果1 [id1, nameAlice, score90], ...array_column()是 PHP 5.5 引入的宝藏函数用于提取一列或者以某列作索引。它接受第三个参数作为返回数组的键配合null作为第二参数可以整行保留。这个函数在处理 API 数据、拼接下拉选项时极其好用。$names array_column($rows, name); // [Alice, Bob, Cathy] $scores array_column($rows, score, name); // [Alice 90, Bob 85, Cathy 95]4.2 用引用遍历二维数组带来的隐患对二维数组做修改有人会写foreach ($rows as $row) { $row[score] 5; } // 没生效因为 $row 是副本 foreach ($rows as $row) { $row[score] 5; } // 生效了但用引用遍历时有一个经典的坑循环结束后必须unset($row)。否则$row还保留着指向数组最后一个元素的引用后续代码里任何对$row变量的复用都会悄悄篡改$rows的最后一个元素。这个 bug 非常隐蔽我见过不止一次在生产代码里出现。foreach ($rows as $row) { $row[score] 5; } unset($row); // 无论如何都别忘了如果不想用引用可以用array_map()$rows array_map(function ($row) { $row[score] 5; return $row; }, $rows);array_map()返回新数组不影响原数组语义清晰推荐大量使用。它的可读性可能稍差但避免了引用的副作用。4.3 数组合并到二维场景array_push vs 直接追加向二维数组追加一行记录有两种常见写法// 写法一 array_push($rows, [id 4, name Dave, score 88]); // 写法二 $rows[] [id 4, name Dave, score 88];推荐写法二。array_push()要多传一次函数名且它返回的是新的元素个数而不是数组本身可读性稍差。唯一使用array_push()的合理场景是一次追加多个元素array_push($arr, $a, $b, $c);5. 数组相关的高频面试题与实战陷阱5.1 面试题empty()、isset()、array_key_exists()的组合判断这类题几乎必考。核心记住下表判断条件isset()empty()array_key_exists()键不存在falsetruefalse键存在值为 nullfalsetruetrue键存在值为 0truetruetrue键存在值为 falsetruetruetrue键存在值为空字符串truetruetrue键存在值为非空字符串truefalsetrue开发里最稳妥的判断存在性用array_key_exists()判断“有实质内容”用!empty()。把两者配合起来用除非你的业务区分“值为 null”和“键不存在”否则一般不会出错。5.2 面试题数组中的“引用与值”语义PHP 数组默认是值语义写时复制。赋值给另一个变量后修改新变量不会影响原数组$a [x 1]; $b $a; $b[x] 2; // $a 还是 [x 1]但如果用引用赋值$a [x 1]; $b $a; $b[x] 2; // $a 变成 [x 2]这个语义在函数传参时也很关键。函数内修改参数数组默认不会影响外部数组。如果想影响可以声明函数参数为引用或者返回新数组。PHP 8.1 里增加了readonly属性但数组本身没有天然的“不可变类型”所以还是要靠编码习惯来保证数据不被意外修改。5.3 实战陷阱unset 后数组指针错乱unset()删除元素后PHP 数组内部指针行为会变得很微妙。举例$arr [a, b, c, d]; unset($arr[1], $arr[2]); print_r($arr); // Array ( [0] a [3] d )后续你再用foreach遍历$arr里只有两个元素不会出现越界或警告。但如果你用for ($i 0; $i count($arr); $i)访问$arr[1]、$arr[2]就会触发“Undefined array key”的 PHP 8 警告。这种 bug 在旧代码迁移到 PHP 8 后尤其常见因为 PHP 8 把“未定义数组键”从 Notice 升级为 Warning日志会被刷爆。解决办法不要依赖数字键连续性。如果确实需要紧凑的数字键在删除后用array_values()重建索引。或者从一开始就避免用数字下标做“位置语义”把数组当成集合而不是列表。5.4 实战技巧用数组模拟栈、队列和集合热词里出现了“动态数组”、“数组队列”等。PHP 的数组可以非常方便地模拟常见数据结构栈array_push()入栈array_pop()出栈。队列array_push()入队array_shift()出队小数组没问题。集合用array_flip()把值变成键然后靠isset()判断是否存在时间复杂度 O(1)。// 集合判断 $set array_flip([apple, banana, orange]); if (isset($set[apple])) { echo exists; }对于大数据量超过几万条的队列操作array_shift()的 O(n) 代价不可忽略。这时候可以使用SplQueue或SplDoublyLinkedList它们的内存效率和操作速度都远优于用 PHP 数组模拟的队列。但说实话日常 Web 业务里很少碰到这种量级如果真遇到了优先考虑 Redis 的 List 结构而不是在 PHP 进程内耗内存。6. 实战案例做一个迷你图书管理系统6.1 需求定义与数据结构设计花了这么多篇幅讲理论现在用一个小实战把这些串起来做一个基于数组的迷你图书管理系统支持图书的新增、删除、按分类筛选、按价格排序、统计总数和平均价格。先把数据结构定义出来$books [ [id 1, title PHP 8 编程, category PHP, price 89], [id 2, title JavaScript 高级程序设计, category JS, price 128], [id 3, title MySQL 必知必会, category DB, price 49], [id 4, title Laravel 框架入门, category PHP, price 69], ];这个二维数组是业务里最常见的形态下面的操作都围绕它展开。6.2 增加图书与编号分配新增图书时手动指定id容易撞车。用max()从现有数组里找最大id再 1是数组场景下的简单做法function addBook(array $books, string $title, string $category, float $price): void { $maxId 0; foreach ($books as $book) { if ($book[id] $maxId) { $maxId $book[id]; } } $books[] [ id $maxId 1, title $title, category $category, price $price, ]; } addBook($books, 设计模式之美, PHP, 99);这里函数参数用引用$books就是为了直接在原数组上追加返回值设为void。如果你不喜欢引用也可以让函数返回新数组看团队风格。但无论哪种方式max(array_column($books, id)) 1是更简洁的写法$newId max(array_column($books, id)) 1;6.3 按分类筛选与按价格排序筛选用array_filter()排序用usort()组合在一起就能应付大多数列表页需求function getBooksByCategory(array $books, string $category): array { return array_filter($books, function ($book) use ($category) { return $book[category] $category; }); } function sortBooksByPrice(array $books, string $order asc): void { usort($books, function ($a, $b) use ($order) { if ($order desc) { return $b[price] $a[price]; } return $a[price] $b[price]; }); }array_filter()返回新数组键会保留原键。如果想重置数字索引在外面包一层array_values()。6.4 统计与数据清洗统计总数、平均价格用array_sum()和count()$total count($books); $avgPrice array_sum(array_column($books, price)) / $total;删除某本书用unset或者array_filter按 id 过滤$idToDelete 3; $books array_values(array_filter($books, function ($book) use ($idToDelete) { return $book[id] ! $idToDelete; }));这样处理还有个好处数据本身带id字段即使数组重建索引也不会破坏数据的完整性。这也是我反复强调“不要把数组下标当成业务主键”的原因。7. 几个常见报错与排查建议7.1 “Undefined array key” 报错PHP 8 以后访问不存在的数组键会抛出Warning: Undefined array key。这不是致命错误但会污染日志、影响接口调试。排查思路用array_key_exists()或isset()做前置判断。用??null 合并运算符提供默认值。$name $data[name] ?? 默认值一行搞定。如果从 API 接收数据先用is_array($data)确认类型再做键访问。7.2 安装 PHP 扩展时报 “no package libzip found”这个报错不在数组语法里但热词里出现了。安装 PHP 扩展比如 zip时源码编译阶段会依赖系统库常见于使用宝塔面板或手动编译 PHP 的场景。解决方案是安装对应的系统依赖库# CentOS/RHEL yum install libzip-devel # Ubuntu/Debian apt-get install libzip-dev安装后再重新执行phpize ./configure make make install。这个问题的本质是“编译环境缺依赖包”和 PHP 本身的数组特性无直接关系但此类环境问题是初学者最容易卡住的地方顺带记录一下。7.3 Windows 下 PHP 报 “vcruntime140.dll 14.0 is not compatible”在 Windows 用 PHP 经常碰到这个报错原因是 PHP 8 在不同版本里依赖的 Visual C Redistributable 版本不一致。热词里写的是“php warning: c:\windows\system32\vcruntime140.dll 14.0 is not compatible”。解决办法是到微软官网下载并安装最新的Visual C Redistributable for Visual Studio 2015-2022然后重启 Web 服务。这类问题用宝塔面板、小皮面板phpstudy时比较容易遇到因为面板自带的 PHP 版本可能对应不同的 VC 运行库版本。换用php -v在命令行测试可以快速定位。7.4 使用 PhpStorm 调试数组的小技巧热词里出现“php 8 phpstorm”。在 PhpStorm 里调试数组有两个非常实用的功能在变量面板里右键数组选择 “Jump to Source”查看 array 相关函数定义。用print_r($arr, true)作为临时日志比直接var_dump()干净输出不会撑爆控制台。PhpStorm 的 Evaluate Expression 窗口Alt F8里可以直接执行array_column($arr, name)这类代码片段实时验证函数效果比反复改代码刷新浏览器高效得多。如果你在命令行调试 PHP 数组我强烈推荐安装phpdbg或直接用php -a进入交互模式配合print_r()试小段逻辑。8. 数组函数的高效使用习惯看到这里普通的增删改查大家应该都会了。最后分享几个我平时写数组相关代码时坚持的习惯不算高深但能少踩很多坑。数组当作“不可变”数据来操作多用array_map()、array_filter()、array_reduce()生成新数组少用引用。代码可读性更好也更安全。能用一行函数解决的不写循环array_column()、array_combine()、array_flip()组合使用能大幅压缩代码量。举例从数据库结果集$rows中生成“id name”的映射$idToName array_column($rows, name, id);这一行的背后是多次 foreach if 判断才能完成的逻辑。写出来又短又不容易错。对类型保持敏感PHP 是弱类型语言数组比较、合并、去重都涉及隐式转换。如果你希望严格按类型处理多用array_filter($arr, is_int)这类显式过滤或者配合array_map做类型转换。写注释记录“为什么”数组操作方法通常一行能看懂但“为什么这么组合”才是信息量最大的地方。比如usort里打破平局的次要键一定要注释说明业务规则是什么否则几个月后你自己看都懵。我还记得有一次线上故障排查了很久最后发现就是一个foreach ($rows as $row)没unset($row)导致后续一个偶然条件里修改了数组最后一个元素。那次之后我对数组引用遍历的警惕性提到了最高。数组这章是 PHP 入门里“信息量最大”的一章因为 PHP 的数组把列表、字典、栈、队列揉在了一起语法看似简单但深度完全够挖。把这些操作函数和边界情况下次遇到不用再到处搜文档。如果你和我一样是“数组恐惧症”关键在于多动手把二维数组当成数据库表格去玩用array_column、array_filter、usort拼管线式的数据处理。熟能生巧慢慢你会发现PHP 的数组可能就是这门语言里最值得爱的那部分。