先说结论:PHP里的二维数组就是数组的数组,取值之前先确认外层数组是数字索引还是字符串键名。大量 Undefined offset 报错并不是循环写得不对,而是把键名和下标混在了一起。比如第一行数据的 name 字段,可能写成 $rows[0]['name'],也可能写成 $rows['first']['name'],两者完全不同,一旦搞混就会取到 null 或直接抛错。

一、直接定位:用下标和键名取出指定元素
二维数组最常见的形态是一个数组内部包含多个结构相同的一维数组。假设有一组用户数据,每个用户都有 id、name、status 三个字段,构建方式如下:
<?php
$users = [
['id' => 1, 'name' => 'Tom', 'status' => 1],
['id' => 2, 'name' => 'Jerry', 'status' => 0],
['id' => 3, 'name' => 'Spike', 'status' => 1],
];
echo $users[0]['name']; // Tom
echo $users[2]['status']; // 1
上面这种结构属于数字索引的二维数组,外层用 0、1、2 定位行,内层用字符串键名定位列。如果外层也是关联数组,比如用用户编号作为键,那么取值方式就变为 $users['u001']['name']。在写代码前先打印 array_keys($users) 看看外层键类型,是避免混淆的最直接办法。
直接下标访问的优点是速度快、语义清晰,适合你已经明确知道数据位置或键名的时候。缺点是如果键不存在,PHP 会抛出 Notice 级别的 Undefined offset 或 Undefined array key 提示。在生产环境中,这类提示虽然不一定中断执行,但会影响日志质量,也可能返回 null 后被后续逻辑误判。因此直接取值时建议配套使用 isset()、array_key_exists() 或空合并运算符。
isset() 会同时判断键是否存在且值不为 null,而 array_key_exists() 只判断键是否存在,即使值为 null 也返回 true。两者的差异在接口数据中很关键。例如某个字段明确返回了 null,你希望走默认值逻辑,此时 isset() 会误判成不存在,array_key_exists() 才更准确。PHP 7 引入的空合并运算符 ?? 可以简化写法:
<?php $name = $users[0]['nickname'] ?? '默认昵称'; echo $name; // 默认昵称
二、遍历二维数组:foreach 嵌套、解构赋值与索引循环
如果要把二维数组中的每一行都处理一遍,最常用的是 foreach。外层循环遍历行,内层循环遍历当前行的键值对,或者直接根据业务读取固定字段。
<?php
foreach ($users as $user) {
echo $user['name'] . ' 状态:' . $user['status'];
}
这种方式简单直接,适合大部分业务场景。但需要注意,如果在循环中修改原数组,默认情况下 $user 只是副本,修改不会反映到 $users 中。只有给 $user 加上引用符号 &,修改才会直接影响原数组。不过引用循环结束后最好调用 unset($user),避免后续代码对同名变量的操作继续影响最后一个元素。
<?php
foreach ($users as &$user) {
$user['status'] = 1;
}
unset($user);
当二维数组的每行结构固定且字段顺序固定时,可以借助 PHP 7.1 开始支持的数组解构,把每一行直接拆成变量。这样可读性更高,适合 CSV 解析、固定列数据等场景。
<?php
foreach ($users as [$id, $name, $status]) {
echo $name;
}
如果外层是数字索引且顺序连续,也可以使用 for 循环配合 count() 遍历。但 for 循环的代码量更大,而且无法处理关联键名和非连续索引,所以只在需要根据下标做特殊计算时使用。
<?php
for ($i = 0; $i < count($users); $i++) {
echo $users[$i]['name'];
}
三、按列取值与条件筛选:array_column、array_filter、array_map
如果只需要二维数组中某一列的所有值,不一定要写两层循环,可以直接使用 array_column()。这是 PHP 5.5 加入的函数,能够从二维数组中提取指定键名对应的值组成新数组。
<?php $names = array_column($users, 'name'); print_r($names); // ['Tom', 'Jerry', 'Spike']
array_column() 的第三个参数可以指定某个字段作为新数组的键。比如用 id 作为键、name 作为值,可以得到一个便于按 id 查找的映射数组。这个技巧在做下拉框选项、ID 到名称的映射时非常实用。
<?php $nameMap = array_column($users, 'name', 'id'); print_r($nameMap); // [1 => 'Tom', 2 => 'Jerry', 3 => 'Spike']
条件筛选则可以使用 array_filter()。它接受一个回调函数,逐行判断是否保留。下面的例子会保留 status 为 1 的用户。
<?php
$activeUsers = array_filter($users, function ($user) {
return $user['status'] === 1;
});
print_r($activeUsers);
需要注意的是,array_filter() 会保留原来的键名。如果筛选后原键名不连续,后续做 json_encode() 时数组可能被编码成对象,而非 JavaScript 数组。此时需要再用 array_values() 重新建立连续数字索引。
<?php $activeUsers = array_values($activeUsers);
如果需要把每一行转换成一个新结构,例如只保留 id 和 name,或拼接字符串,可以使用 array_map()。它比 foreach 更适合一步完成映射。
<?php
$brief = array_map(function ($user) {
return [
'id' => $user['id'],
'name' => $user['name'],
];
}, $users);
print_r($brief);
PHP 7.4 之后还可以使用箭头函数让回调更短:array_map(fn($user) => $user['name'], $users)。箭头函数自动捕获外层变量,在简单场景下更清晰。
四、取值的避坑点:缺失字段、null 判断和索引重建
二维数组的数据通常来自数据库查询、接口返回或用户提交,结构可能并不像示例中那么规整。例如某一行缺少 status 字段,直接写 $user['status'] 就会触发 Undefined array key。面对这种情况,优先在取值入口做归一化处理,或者使用 ?? 提供默认值。
<?php
foreach ($users as $user) {
$status = $user['status'] ?? 0;
echo $status;
}
使用 array_column() 时也一样,如果某些行没有目标键,返回的数组对应位置可能为 null。若后续还需要计数、求和或拼接,建议先过滤空值。比如 array_filter($names) 可以去掉 null、false、空字符串等。不过要注意,array_filter() 默认的过滤规则会一并去掉 0 和 '0',如果这两个值在业务中有意义,就需要自定义回调。
<?php
$names = array_column($users, 'name');
$names = array_filter($names, function ($name) {
return $name !== null;
});
另一个容易忽略的问题是数组键值连续性。比如筛选掉某些行后,得到的数组键可能是 0、2、5,这种数组在 json_encode() 时会变成对象结构 {"0": "...", "2": "..."},前端接收后无法按数组遍历。解决方式就是前面提到的 array_values()。
最后还要区分空值判断的选择。比如要判断一个二维数组是否为空,直接用 empty($users) 即可;判断某行某字段是否存在且有意义,可以组合 isset() 和 array_key_exists()。总结下来,取值方法本身并不复杂,真正影响稳定性的是对数据结构边界情况的处理。把键名、索引、空值和字段缺失这些细节弄清楚,再选择直接访问、遍历、列提取或筛选,就能覆盖绝大多数 PHP 二维数组取值需求。