PHP的array_unique函数去重后索引乱了怎么恢复

发布时间:2026/10/3 1:34:23
PHP的array_unique函数去重后索引乱了怎么恢复 前言array_unique()去重之后索引乱掉是 PHP 新手每天都在踩的坑症状大致有这几种去重前是[0 a, 1 b, 2 a]去重后变成[0 a, 1 b]——这次看起来还好但如果重复的不是末尾元素就会变成[0 a, 2 c]这样带着空洞的数组用for ($i 0; $i count($arr); $i)遍历突然发现漏掉了元素或者报Undefined array key把去重结果交给json_encode()本该输出 JSON 数组结果输出的是对象{0:a,2:c}前端forEach直接报错前端拿到的列表顺序看着没错但第 3 条的id对不上因为索引和顺序已经脱钩了。根本原因只有一个array_unique()的契约是保留键preserve keys。它删除的是键值对不是元素的位置。它不改键名只是把重复项对应的键值对移出数组于是键名出现空洞。本文讲清楚三件事键为什么会被保留、怎么恢复成干净的列表、以及比索引问题更隐蔽的比较规则陷阱——array_unique()在默认情况下用的是字符串比较这会让1、1、true、1.0被判定为同一个值。所有示例在 PHP 8.x 上可直接运行array_is_list()需要 PHP 8.1。一、先理解语义array_unique 保留键不重排?php declare(strict_types1); $input [a, b, a, c, b]; $unique array_unique($input); var_export($unique);输出是array ( 0 a, 1 b, 3 c, )三条性质可以从中读出来性质说明保留键名键0、1、3原样保留中间的空洞不会被填补保留首次出现重复值a出现在 0 和 2保留 0丢弃 2保持原有顺序不去重排序剩下的元素按原顺序排列array_unique()返回的是一个新数组不是原地修改所以必须接收返回值去掉的键是重复值后来出现的位置。所以空洞出现在哪里取决于重复值第一次出现在什么位置。这就解释了两个反直觉现象如果重复值都在数组末尾去重后索引看起来是连续的问题被掩盖到下次数据变化才暴露而一旦重复值出现在中间for循环立刻漏元素。判断一个去重结果是否干净列表的标准工具是array_is_list()PHP 8.1 引入var_dump(array_is_list([a, b, c])); // true var_dump(array_is_list([0 a, 1 b, 3 c])); // false二、三种恢复方式各有取舍2.1 array_values()最直接$clean array_values(array_unique($input)); // [0 a, 1 b, 2 c]array_values()丢弃所有键、重新分配0..n-1。它就是索引乱了怎么恢复的标准答案。2.2 重建列表在需要自定义保留规则时用有时你不想保留第一次出现而是想保留最后一次出现比如后来者覆盖前者。这时array_unique()不适用需要手动重建?php declare(strict_types1); /** 按值去重保留最后一次出现并重新索引 */ function uniqueKeepLast(array $items): array { $seen []; foreach ($items as $item) { $key is_scalar($item) || $item null ? gettype($item) . : . var_export($item, true) // 区分 1 与 1 : json: . json_encode($item); $seen[$key] $item; // 同键覆盖最终留下最后一个 } return array_values($seen); // 顺序会变成最后一次出现的顺序 }这里用gettype() . : . var_export()构造哈希键hash key把类型也纳入比较就顺手解决了下一节要讲的类型混淆问题。2.3 array_flip() 组合只在特定条件下可用array_flip()加两次翻转也能去重$clean array_keys(array_flip($input)); // [a,b,c]但要小心这条路限制很多array_flip()只能处理字符串和整数值遇到null、数组、对象会警告或直接失败array_keys()回来的键是整数或字符串——如果原始值是纯数字字符串比如123PHP 会把它变成整数123类型悄悄变了。除非你确定数组里只有非数字字符串否则别用这招。三、真正的深坑默认比较规则是字符串比较上面都是索引层面的问题好解决。真正让人排查半天的是比较规则。array_unique()的第二个参数$flags默认是SORT_STRING意思是先把两个元素转成字符串再比较。?php declare(strict_types1); $mixed [1, 1, true, 1.0, 01]; var_export(array_unique($mixed));输出只剩两个元素array ( 0 1, 4 01, )推导过程是这样(string) 1是1(string) 1是1(string) true也是1(string) 1.0同样是1。所以数组里键 0~3 这四个位置的值在 SORT_STRING 下全部是同一个字符串1只有第一个键 0整数1被保留01转出来是01与1不同因此被保留。四个不同类型、不同含义的值去重后只剩两个——如果你的数组里混着整数 ID 和字符串 ID或者混着true和1数据就这么无声无息地少了而且不会有任何警告。换个$flags常量能改变比较方式但没有任何一个常量能做到严格比较常量比较方式1与11与trueSORT_STRING默认转成字符串后比较视为相同视为相同SORT_REGULAR按 PHP 的类型转换规则比较等价于视为相同视为相同SORT_NUMERIC转成数字后比较视为相同视为相同SORT_LOCALE_STRING按当前区域设置转成字符串后比较取决于区域取决于区域这张表里每一行都是视为相同正是问题所在SORT_REGULAR用的是宽松比较它比SORT_STRING更宽松比如01 1也成立而不是更严格。所以想要类型也参与比较的去重array_unique()给不了只能自己写。这也是本文把上面那个uniqueKeepLast()用gettype()拼哈希键的原因。顺带说一个 PHP 8.0 的变更0 foo这类数字与不可解析字符串的比较在 PHP 8.0 起结果从true变成false。这会影响SORT_REGULAR下对混合类型数组的去重结果——升级到 8.x 之后同一个数组的去重结果可能与 7.4 不一致。排查升级后数据变多了的问题时这是一个值得回头检查的点。四、实战一个可直接运行的严格去重工具下面这个脚本演示了索引恢复、类型敏感去重、二维数组按字段去重三种场景?php declare(strict_types1); // 最低要求PHP 8.1用到 array_is_list /** * 严格去重类型与值都相同才算重复保留第一次出现返回干净的列表。 * 注意这里只演示标量与 null复杂结构请先用规范化函数转成稳定字符串。 */ function uniqueStrict(array $items): array { $seen []; $result []; foreach ($items as $item) { $key is_object($item) ? object: . spl_object_id($item) : gettype($item) . : . var_export($item, true); if (isset($seen[$key])) { continue; // 已经出现过跳过 } $seen[$key] true; $result[] $item; // 用 [] 追加天然是连续索引 } return $result; } /** 按二维数组的某个字段去重返回干净列表 */ function uniqueByField(array $rows, string $field): array { $seen []; $result []; foreach ($rows as $row) { $value $row[$field] ?? null; $key gettype($value) . : . var_export($value, true); if (isset($seen[$key])) { continue; } $seen[$key] true; $result[] $row; } return $result; } // ---- 场景 1索引恢复 ---- $raw [a, b, a, c, b]; $u array_unique($raw); printf(array_unique 原始结果: %s\n, json_encode($u)); printf(array_values 之后: %s\n, json_encode(array_values($u))); printf(是否列表: %s - %s\n, json_encode($u), array_is_list($u) ? 是 : 否); printf(json_encode 原始结果: %s\n, json_encode($u)); // 对象形式 printf(json_encode 恢复之后: %s\n, json_encode(array_values($u))); // ---- 场景 2类型敏感 ---- $mixed [1, 1, true, 1.0, 01, 1]; printf(默认 SORT_STRING: %s\n, json_encode(array_unique($mixed))); printf(严格模式: %s\n, json_encode(uniqueStrict($mixed))); // ---- 场景 3按字段去重 ---- $rows [ [id 1, email aexample.com, name A1], [id 2, email bexample.com, name B], [id 3, email aexample.com, name A2], // 重复邮箱 ]; printf(按 email 去重: %s\n, json_encode( uniqueByField($rows, email), JSON_UNESCAPED_UNICODE )); // ---- 场景 4不去重但要压缩索引 ---- $sparse [5 x, 9 y, 20 z]; printf(稀疏数组转列表: %s\n, json_encode(array_values($sparse)));运行结果里最值得留意的是场景 1 的第三行json_encode(array_unique($raw))输出的是{0:a,1:b,3:c}这种对象形式而加一层array_values()之后才是[a,b,c]。这就是前后端联调时接口返回的数组变成了对象的根源。常见坑点1. 去重后仍然用for循环按下标遍历// ❌ 键 2 被删掉后$i2 这一轮访问不存在的键还会漏掉后面的元素 $u array_unique($list); for ($i 0; $i count($u); $i) { echo $u[$i]; }// ✅ 要么先恢复索引再按下标访问要么直接用 foreach $u array_values(array_unique($list)); foreach ($u as $v) { echo $v; }2. 忘记接住返回值// ❌ array_unique 不修改原数组调用完之后数组还是老样子 array_unique($list); var_dump($list);// ✅ $list array_values(array_unique($list));3. 去重后没有重新索引直接交给 json_encode// ❌ 输出 {0:a,1:b,3:c}前端 array.forEach 报 forEach is not a function echo json_encode(array_unique($list));// ✅ 输出 [a,b,c] echo json_encode(array_values(array_unique($list)));4. 在混合类型数组上依赖默认比较// ❌ 1、1、true、1.0 会被当成同一个值数据静默减少 $ids [1, 1, 2]; count(array_unique($ids)); // 2字符串 1 消失了// ✅ 需要类型敏感时自己写严格去重见 uniqueStrict或先把数据统一成同一类型 $ids array_map(intval(...), $ids); $ids array_values(array_unique($ids, SORT_REGULAR));5. 用 array_unique 处理二维数组// ❌ 默认 SORT_STRING 下数组无法转字符串触发 Array to string conversion 警告 // 并且所有元素都被当成同一个字符串 Array去重后只剩一条 $rows [[id 1], [id 2]]; array_unique($rows);// ✅ 想比较整个子数组用 SORT_REGULAR想按字段去重用 uniqueByField $rows array_values(array_unique($rows, SORT_REGULAR));6. 用 array_unique 处理对象数组// ❌ SORT_STRING 下对象无法转字符串Error: Object of class User could not be converted to string $users [$u1, $u2, $u1]; array_unique($users);// ✅ 按对象标识去重保留第一次出现的实例 $unique []; foreach ($users as $u) { $unique[spl_object_id($u)] $u; } $unique array_values($unique);7. 以为 array_unique 会保持键值的映射关系// ❌ 期待 [a 1, b 1] 去重后 key 也一起没掉实际是丢掉了 b 这一对 $map [a 1, b 1, c 2]; var_export(array_unique($map)); // array(a 1, c 2)键 b 消失了// ✅ 如果需要按键去重那是另一回事用 array_diff_key / array_flip 思路处理 $byKey array_diff_key($map, [b null]);8. 把 array_unique 与 sort 记混以为它会排序// ❌ array_unique 不排序去重结果的顺序是首次出现顺序不是字典序 var_export(array_unique([c, a, c, b])); // array(0 c, 1 a, 3 b)// ✅ 需要排序就显式排sort 是原地修改、返回 bool别写成 $x sort($arr) $u array_values(array_unique([c, a, c, b])); sort($u);总结需求写法说明去重后恢复连续索引array_values(array_unique($arr))最常用一行解决保留原键名array_unique($arr)默认行为键会留空洞想拿到被保留元素的原始键array_keys(array_unique($arr))结果是键的列表而非值的列表类型敏感去重自建哈希表 gettype()前缀array_unique没有严格模式保留最后一次出现自建哈希表后array_values()array_unique只保留首次出现二维数组按字段去重自建哈希表 array_values()不能用array_unique确认结果是列表array_is_list($arr)PHP 8.1 起可用一句话结论array_unique()不是去重后就变成新数组而是保留键名地移除重复项。只要记住两件事——在它外面套一层array_values()以及它默认按字符串比较——索引混乱和值被意外合并这两类问题就都能躲开。如果需要类型敏感或按字段去重就自己建一个哈希表并显式$result[] 追加比在array_unique的$flags上试参数可靠得多。