PHP 数组去重最简单的写法是 array_unique(),一行就能去掉重复值。但它有个容易被忽略的细节:去重后仍然保留原来的键名,下标会出现断层,所以后面通常还要再套一层 array_values() 重新索引。如果面对的是二维数组,直接对它用 array_unique 反而得不到正确结果,必须按指定字段去重。
新手写数组去重时常撞上两个怪事:一是去重后 foreach 循环莫名少读到元素,二是对二维数据去重后结果全乱了。前者是键名断层导致的,后者是二维数组没法整体比较导致的。今天这篇文章,编程狮就把 PHP 数组去重的 4 种写法一次讲清:从最小可运行示例开始,逐个说明适用条件和输出差异,最后重点解决二维数组按字段去重这个高频难题。

一、数组去重到底在去什么
数组去重,指的是把数组中重复出现的值删掉,只保留其中一份。听起来很简单,但有三个细节决定了你该选哪种写法:
- 去重后是否保留键名 —— 多数内置函数只删值不动键,下标会断;
- 按什么标准算重复 —— 默认按字符串比较,整数 4 和字符串 "4" 会被判成同一个值;
- 元素是标量还是数组 —— 二维数组无法整体比较,必须指定按哪个字段去重。
1.1 一个能立刻运行的最小示例
先看最基础的一维数组去重:
<?php
$tags = ['php', 'js', 'php', 'java', 'js'];
$result = array_unique($tags);
print_r($result);
输出如下,请特别留意左边的键名:
Array
(
[0] => php
[1] => js
[3] => java
)
值确实去重了,但键名跳过了 2 和 4。这就是保留键名带来的断层,第三节会讲怎么处理。
1.2 常见误解
误解一:以为 array_unique 处理后下标会自动重排。 不会。它只删掉重复的元素,剩下的键原样保留——因为原数组的键本身可能携带信息,函数不会擅自改动。
误解二:以为 array_unique 能直接处理二维数组。 直接传进去会触发 Array to string conversion 的提示,结果也不可预期。二维数组去重必须换思路,见第四节。
误解三:以为去重后第一个元素下标一定是 0。 如果原数组本来就不是从 0 开始的,或者去重时正好删掉了第一个元素,下标 0 可能根本不存在。
二、array_unique():最常用的一行去重
这是日常用得最多的写法,一行搞定:
<?php
$tags = ['php', 'js', 'php', 'java', 'js'];
// 只去重:保留原键名,下标会断层
$result = array_unique($tags);
print_r($result); // [0 => php, 1 => js, 3 => java]
// 去重并重新索引,这才是项目里最常用的组合
$result = array_values(array_unique($tags));
print_r($result); // [0 => php, 1 => js, 2 => java]
为什么键名会断?因为 array_unique 的语义是"在原数组上删掉重复项",它保留键名是有意为之——关联数组的字符串键往往有意义,不能随便重建。想要连续下标就显式套一层 array_values,这是最稳妥的习惯。
第二个参数是比较方式,默认是 SORT_STRING。 这意味着比较前元素会被转成字符串,于是整数 4 和字符串 "4" 会被当成同一个值:
<?php
$input = [4, "4", "3", 4, 3, "3"];
$result = array_unique($input);
var_dump($result);
array(2) {
[0]=> int(4)
[2]=> string(1) "3"
}
六个元素最后只剩两个。如果你认为整数 4 和字符串 "4" 不该算重复,就要显式指定比较方式,例如 array_unique($input, SORT_REGULAR)。
需要注意的是,按字符串比较对大数组有一份额外的转换开销。数组里如果只有普通字符串或整数,第三节的翻转法通常更快;如果元素是数组或对象,则不要用 array_unique。想把这些数组函数一次补齐,可以顺着 PHP 数组用法 系统过一遍。
三、array_flip() 翻转两次与手动遍历去重
3.1 array_flip() 翻转两次
PHP 数组的键天然唯一。利用这一点可以巧妙去重:先用 array_flip 把值变成键,重复的值互相覆盖,再翻回来就只剩不重复的元素。
<?php
$tags = ['a' => 'php', 'b' => 'js', 'c' => 'php'];
$result = array_flip(array_flip($tags));
print_r($result);
Array
(
[c] => php
[b] => js
)
两个细节要留意。第一,保留下来的是最后一次出现的那个键,也就是 c 而不是 a,这和 array_unique 保留首次出现正好相反。第二,array_flip 只接受字符串和整数作为值,数组里若含数组或对象,会触发 Can only flip STRING and INTEGER values 警告。所以这个方法只适合纯标量值的一维数组。
3.2 foreach 配合 in_array 手动去重
最直观、也最可控的写法是自己遍历一遍,用新数组收集没出现过的元素:
<?php
$tags = ['php', 'js', 'php', 'java', 'js'];
$result = [];
foreach ($tags as $tag) {
if (!in_array($tag, $result)) {
$result[] = $tag;
}
}
print_r($result); // [0 => php, 1 => js, 2 => java]
因为是逐个追加,结果下标天然连续,不用再调 array_values。给 in_array 传第三个参数 true 可开启严格比较,同时比较类型和值,避免 4 和 "4" 被误判为同一个值:
if (!in_array($tag, $result, true)) {
3.3 array_reduce 函数式写法
想把去重逻辑收成一个表达式,可以用 array_reduce。它接收一个回调函数,把数组逐步归并成一个结果:
<?php
$tags = ['php', 'js', 'php', 'java', 'js'];
$result = array_reduce($tags, function ($carry, $item) {
if (!in_array($item, $carry)) {
$carry[] = $item;
}
return $carry;
}, []);
print_r($result); // [0 => php, 1 => js, 2 => java]
回调函数的两个参数分别是累积结果和当前元素,末尾的 [] 是初始值。这个写法逻辑集中、便于复用,缺点是每判断一个元素都要遍历累积数组,数据量大时不如前两种快。
四、二维数组按字段去重的正确做法
这是项目里最常遇到的需求:从数据库查出一批用户,想按城市字段去掉重复,但保留每行的完整信息。直接丢给 array_unique 行不通——它会试图把每个内层数组转成字符串来比较,结果不可预期。
正确做法分三步:先把要去重的那一列取出来,对这一列去重,再按留下来的键取回完整的行。
<?php
$users = [
['id' => 1, 'name' => '张三', 'city' => '北京'],
['id' => 2, 'name' => '李四', 'city' => '上海'],
['id' => 3, 'name' => '王五', 'city' => '北京'],
];
// 第一步:取出 city 这一列
$cities = array_column($users, 'city'); // ['北京', '上海', '北京']
// 第二步:对这一列去重,保留首次出现的键
$uniqueCities = array_unique($cities); // [0 => '北京', 1 => '上海']
// 第三步:按这些键取回完整行,并重新索引
$result = array_values(array_intersect_key($users, $uniqueCities));
print_r($result);
Array
(
[0] => Array
(
[id] => 1
[name] => 张三
[city] => 北京
)
[1] => Array
(
[id] => 2
[name] => 李四
[city] => 上海
)
)
王五那一行被去掉了,因为他的城市"北京"已经出现过。这里的关键函数是 array_intersect_key,它按键名取交集,正好把"去重后留下了哪些键"映射回完整的数据行。
另一种写法是用 array_reduce 按字段建索引。 拿字段值当临时键,同字段的行自然会互相覆盖:
<?php
$result = array_values(array_reduce($users, function ($carry, $item) {
// 不加判断会保留最后一条(王五);加了 ?? 则保留第一条(张三)
$carry[$item['city']] = $carry[$item['city']] ?? $item;
return $carry;
}, []));
这里的 ?? 很关键:$carry[$item['city']] ?? $item 表示"这个城市还没存过就存当前行,存过就保持原样",于是保留的是首次出现的那一行。把 ?? 去掉直接赋值,保留的就是最后一行,两种行为按需选择。
五、怎么选:4 种数组去重方法对比
四种写法摆在一起,用下面这张表对照自己的数据即可:
| 方法 | 适用数据 | 键名处理 | 保留哪一条 |
|---|---|---|---|
| array_unique | 一维,标量值 | 保留原键名,会断层 | 首次出现 |
| array_flip 翻转两次 | 一维,仅字符串或整数 | 保留最后一条的键 | 最后一次出现 |
| foreach + in_array | 任意一维 | 重新索引,下标连续 | 首次出现 |
| array_reduce | 任意,含二维 | 取决于写法 | 可自行控制 |
再补四条踩坑清单:
第一,array_unique 之后记得 array_values,除非你确实需要保留原键名。
第二,默认按字符串比较,整数与字符串数字会被判为重复,需要区分时显式传 SORT_REGULAR。
第三,二维数组不要直接丢给 array_unique 或 array_flip,先用 array_column 取出字段再处理。
第四,in_array 默认是非严格比较,判断 0、false、空字符串这类值时要传第三个参数 true。

总结
PHP 数组去重的选型可以概括成一句话:一维标量用 array_unique,记得补 array_values;纯字符串整数追求速度用 array_flip 翻转两次;要精细控制用 foreach 或 array_reduce;二维数组按字段去重则用 array_column 配合 array_intersect_key。
- array_unique 保留键名,直接 foreach 可能读漏元素;
- 默认 SORT_STRING 会把整数 4 和字符串 "4" 判为重复;
- array_flip 只认字符串和整数,且保留的是最后一次出现;
- 二维数组去重要先取字段列,不能整体比较。
把数组这块基础打牢,可以顺着 PHP 教程 的数组章节继续往下读。
延伸学习
- PHP 基础配套课程 —— 从语法基础讲到数组与函数的实际运用。
- PHP 数组操作笔记 —— 把排序、过滤、合并等高频数组操作一次列清。
- 文本去重工具 —— 不想写代码时,用它快速去掉文本里的重复行。
常见问题
Q:array_unique 之后为什么 foreach 少读了一个元素?
A:因为 array_unique 保留原来的键名,去重后下标不连续。如果你用 for 循环按下标递增访问,就会跳过被删掉的位置,甚至访问到不存在的下标。解决办法是用 foreach 遍历,或者在去重后加一层 array_values 重新索引,让下标从 0 连续排列。
Q:二维数组去重为什么结果不对?
A:因为 array_unique 默认把元素转成字符串再比较,而数组转字符串只会得到 Array 并伴随一条转换提示,所有内层数组比较起来都一样,去重结果自然不可预期。正确做法是先用 array_column 取出要去重的字段列,对这一列做 array_unique,再用 array_intersect_key 按保留下来的键取回完整的行。
Q:array_flip 报 Can only flip STRING and INTEGER values 怎么办?
A:这条警告说明数组里存在既不是字符串也不是整数的值,比如内层数组或对象,而 PHP 的数组键只能是字符串或整数。此时请换用 array_unique,或者先用 array_column 把需要的字段提取成一维数组再翻转。如果一定要保留 array_flip 的写法,就得先确保数组元素都是标量值。
Q:想让去重后的键名从 0 开始连续,怎么做最省事?
A:最省事的是在 array_unique 外面套一层 array_values,写成 array_values(array_unique($arr)),一步得到下标连续的新数组。另一个办法是改用 foreach 配合 in_array 手动收集,追加元素时下标本来就是连续的,不需要额外处理。

免费 AI IDE



