返回

PHP 数组索引重排:`array_values` 等方法告别跳跃键名

php

PHP 数组索引重排:告别跳跃的键名

处理 PHP 数组时,你可能会碰到这么个情况:用 unset 函数删除数组里的一些元素后,发现数组的键名(索引)变得不连续了。就像下面这样:

<?php
$myArray = [
  1 => [ 'pid' => '915', 'size' => '2', 'count' => 11, 'fee' => '5500' ],
  4 => [ 'pid' => '914', 'size' => '1', 'count' => 8, 'fee' => '1500' ],
  6 => [ 'pid' => '913', 'size' => '2', 'count' => 5, 'fee' => '3200' ],
];

// 假设我们之前 unset 掉了索引 0, 2, 3, 5
// 现在打印 $myArray,会看到:
/*
array(3) {
  [1]=>
  array(4) {
    ["pid"]=> string(3) "915"
    ["size"]=> string(1) "2"
    ["count"]=> int(11)
    ["fee"]=> string(4) "5500"
  }
  [4]=>
  array(4) {
    ["pid"]=> string(3) "914"
    ["size"]=> string(1) "1"
    ["count"]=> int(8)
    ["fee"]=> string(4) "1500"
  }
  [6]=>
  array(4) {
    ["pid"]=> string(3) "913"
    ["size"]=> string(1) "2"
    ["count"]=> int(5)
    ["fee"]=> string(4) "3200"
  }
}
*/
?>

这数组索引是 1、4、6,看着就别扭。要是想让它们变成整整齐齐的 0、1、2,该怎么办呢?

为什么会这样?unset 的行为分析

要搞清楚怎么解决,先得明白为什么 unset 会造成这个问题。

unset 在 PHP 里干的活儿很直接:就是把你指定的变量或者数组元素给“销毁”掉。对于数组元素来说,它会移除那个键(key)和对应的值(value)。

关键点在于:unset 只负责删除,它不会主动去重新整理数组剩下的元素的键名 。它觉得,既然你指定要删掉索引 2 的元素,那我就照做,至于索引 34 还保持原样,这不是它的职责范围。所以,当你 unset 掉一个数字索引数组的某些元素后,剩下的元素索引自然就可能出现空缺,变得不连续了。

这跟数组的类型也有点关系。对于关联数组(用字符串做键名的),unset 掉一个元素通常不会引起“不连续”的困扰,因为我们本来就不期待它的键名是按数字顺序排的。但对于数字索引数组,我们往往希望它的索引是紧密排列的,这时候 unset 带来的空缺就很碍眼了。

解决方案:让索引听话

别担心,让这些跳跃的索引变得服服帖帖有好几种办法。下面介绍几种常用且有效的方案。

方案一:array_values() - 简单直接

这是最常用,也往往是最推荐的方法,专门用来解决这个问题。

原理与作用

array_values() 函数的作用非常纯粹:它接收一个数组,然后返回一个只包含原数组所有值 的新数组。重点来了,这个新数组的键名会被重新建立 ,并且总是从 0 开始的连续数字索引(0, 1, 2, ...)。它根本不关心原来的键名是什么,一股脑儿把值取出来,排好队,贴上新标签。

代码示例

接续上面的例子,用 array_values() 处理一下:

<?php
$myArray = [
  1 => [ 'pid' => '915', 'size' => '2', 'count' => 11, 'fee' => '5500' ],
  4 => [ 'pid' => '914', 'size' => '1', 'count' => 8, 'fee' => '1500' ],
  6 => [ 'pid' => '913', 'size' => '2', 'count' => 5, 'fee' => '3200' ],
];

// 使用 array_values() 获取值并重新索引
$reIndexedArray = array_values($myArray);

print_r($reIndexedArray);

/*
输出结果:
array(3) {
  [0]=>
  array(4) {
    ["pid"]=> string(3) "915"
    ["size"]=> string(1) "2"
    ["count"]=> int(11)
    ["fee"]=> string(4) "5500"
  }
  [1]=>
  array(4) {
    ["pid"]=> string(3) "914"
    ["size"]=> string(1) "1"
    ["count"]=> int(8)
    ["fee"]=> string(4) "1500"
  }
  [2]=>
  array(4) {
    ["pid"]=> string(3) "913"
    ["size"]=> string(1) "2"
    ["count"]=> int(5)
    ["fee"]=> string(4) "3200"
  }
}
*/
?>

看!输出结果里的数组索引已经是整齐的 0, 1, 2 了。原来的键名 1, 4, 6 彻底不见了,达到了我们的目标。

优缺点

  • 优点: 代码简洁,意图明确,性能通常很好。专门为提取值并创建数字索引数组而生。
  • 缺点: 它创建了一个全新的数组。如果原数组非常大,可能会消耗更多内存。不过对于大多数场景,这不是问题。它完全丢弃了原始的键名,但在这个问题场景下,这正是我们想要的。

进阶使用技巧

array_values() 经常和 array_filter() 配合使用。当你用 array_filter() 根据某些条件筛选数组元素后,得到的数组键名也可能不连续。紧接着用 array_values() 就能快速获得一个索引连续的结果数组。

<?php
$scores = [0 => 59, 1 => 95, 2 => 45, 3 => 88, 4 => 70];

// 筛选出及格的分数 (>= 60)
$passedScores = array_filter($scores, function($score) {
    return $score >= 60;
});

// print_r($passedScores); 此时输出可能是 [1 => 95, 3 => 88, 4 => 70],索引不连续

// 用 array_values() 重新索引
$reIndexedPassedScores = array_values($passedScores);

print_r($reIndexedPassedScores);
/*
输出结果:
array(3) {
  [0]=> int(95)
  [1]=> int(88)
  [2]=> int(70)
}
*/
?>

方案二:手动循环构建新数组

如果你想更精细地控制过程,或者觉得 array_values() 不够“透明”,可以自己写个循环来创建新数组。

原理与作用

基本思路是:创建一个空数组,然后遍历原始数组(那个键名不连续的数组)。在循环里,把原数组的每个 (注意,是值,不是键值对)添加到新数组中。当你用 [] 语法向数组添加元素而不指定键名时,PHP 会自动为它分配下一个可用的连续数字索引,通常是从 0 开始。

代码示例

<?php
$myArray = [
  1 => [ 'pid' => '915', 'size' => '2', 'count' => 11, 'fee' => '5500' ],
  4 => [ 'pid' => '914', 'size' => '1', 'count' => 8, 'fee' => '1500' ],
  6 => [ 'pid' => '913', 'size' => '2', 'count' => 5, 'fee' => '3200' ],
];

$reIndexedArray = []; // 初始化一个空数组

// 遍历原数组,只取值
foreach ($myArray as $value) {
  $reIndexedArray[] = $value; // 将值追加到新数组,PHP自动分配 0, 1, 2... 索引
}

print_r($reIndexedArray);
// 输出结果和 array_values() 方法一样
/*
array(3) {
  [0]=>
  array(4) { ... }
  [1]=>
  array(4) { ... }
  [2]=>
  array(4) { ... }
}
*/
?>

这种方法同样能得到索引从 0 开始的连续数组。

优缺点

  • 优点: 代码逻辑非常清晰,容易理解每一步在做什么。可以在循环内部加入额外的处理逻辑(比如修改值、根据条件决定是否添加等)。
  • 缺点: 比起内建函数 array_values(),代码稍微冗长一些。对于非常大的数组,纯 PHP 循环的性能可能略低于 C 语言实现的内建函数。

进阶技巧

这种手动循环的方式提供了极大的灵活性。假设你在重新索引的同时,还想给每个子数组添加一个表示原始索引的字段,就可以这样做:

<?php
$myArray = [
  1 => [ 'pid' => '915', 'size' => '2', 'count' => 11, 'fee' => '5500' ],
  4 => [ 'pid' => '914', 'size' => '1', 'count' => 8, 'fee' => '1500' ],
  6 => [ 'pid' => '913', 'size' => '2', 'count' => 5, 'fee' => '3200' ],
];

$reIndexedArrayWithOriginKey = [];
foreach ($myArray as $originalKey => $value) {
  $value['original_key'] = $originalKey; // 在子数组中添加一个字段记录原始键名
  $reIndexedArrayWithOriginKey[] = $value; // 追加到新数组,获得新索引
}

print_r($reIndexedArrayWithOriginKey);
/*
输出结果:
array(3) {
  [0]=>
  array(5) {
    ["pid"]=> string(3) "915"
    ["size"]=> string(1) "2"
    ["count"]=> int(11)
    ["fee"]=> string(4) "5500"
    ["original_key"]=> int(1) // 看这里!
  }
  [1]=>
  array(5) {
    ["pid"]=> string(3) "914"
    // ...
    ["original_key"]=> int(4) // 看这里!
  }
  [2]=>
  array(5) {
    ["pid"]=> string(3) "913"
    // ...
    ["original_key"]=> int(6) // 看这里!
  }
}
*/
?>

这就体现了手动循环的灵活性,array_values() 可没法直接这么干。

方案三:array_splice() 的“间接”应用 (特定场景)

array_splice() 主要功能是在数组中移除、替换或插入元素,并且它有一个副作用:它会重新索引数组中受影响部分之后的元素 。我们可以利用这个特性,但通常不是解决这个特定问题的首选,因为它有点绕。

原理与作用

你可以用 array_splice() “移除”数组的 0 个元素,从索引 0 开始。虽然什么都没移除,但这个操作会触发 PHP 对整个数组(或至少是受影响部分)进行重新索引,使其变为连续的数字键名。

代码示例

<?php
$myArray = [
  1 => [ 'pid' => '915', 'size' => '2', 'count' => 11, 'fee' => '5500' ],
  4 => [ 'pid' => '914', 'size' => '1', 'count' => 8, 'fee' => '1500' ],
  6 => [ 'pid' => '913', 'size' => '2', 'count' => 5, 'fee' => '3200' ],
];

// 从索引 0 开始,移除 0 个元素。这会触发重新索引
array_splice($myArray, 0, 0, []);

print_r($myArray); // 注意:array_splice 是直接修改原数组的!
// 输出结果与前两种方法相同
/*
array(3) {
  [0]=>
  array(4) { ... }
  [1]=>
  array(4) { ... }
  [2]=>
  array(4) { ... }
}
*/
?>

优缺点

  • 优点: 直接在原数组上操作,不需要创建新数组,可能在内存使用上稍微高效一点(取决于 PHP 内部实现)。
  • 缺点: 代码意图不如 array_values() 清晰。array_splice($arr, 0, 0) 看起来有点像魔法咒语,可读性稍差。主要是用其副作用,而非其核心功能。对于新手来说可能比较困惑。性能上未必比 array_values 快。

注意事项

array_splice 是直接修改 $myArray 本身的,而 array_values 和手动循环创建的是新数组,原数组 $myArray 保持不变(除非你把结果赋回给 $myArray)。这点差异在使用时要特别留意。

方案四:JSON 编码再解码 (曲线救国)

还有个利用 JSON 转换特性的方法,虽然不常用,但也算个思路。

原理与作用

PHP 数组在 json_encode() 时,如果它是一个标准的、从 0 开始的连续数字索引数组,会被编码为 JSON 数组 [...]。但如果它的键名不连续或者是关联数组,通常会被编码为 JSON 对象 {...}。不过,对于我们这种情况(只有非连续数字键),json_encode 通常还是会把它当作一个稀疏数组处理,可能依然输出成 JSON 对象。

然而,当我们把一个表示“列表”(即使键名不连续)的 PHP 数组 json_encode(),再用 json_decode() 解码回来时,如果 json_decode 的第二个参数是 false 或省略(默认返回对象),效果不明显。但如果第二个参数是 true(要求返回关联数组),解码器会尝试将 JSON 数组 [...] 转回 PHP 的 0 索引数组。

更稳妥的做法是先用 array_values (没错,又回到它了) 强制转成值列表,再编码解码,但这就没意义了。一个稍微不同的 JSON 技巧是,某些情况下 json_decode(json_encode($array), true) 能碰巧得到重排索引的效果,但这依赖具体数据结构和 PHP 版本行为,不保证稳定。

一个相对更可靠(但也更绕)的 JSON 相关的做法可能是:

<?php
$myArray = [
  1 => [ 'pid' => '915', 'size' => '2', 'count' => 11, 'fee' => '5500' ],
  4 => [ 'pid' => '914', 'size' => '1', 'count' => 8, 'fee' => '1500' ],
  6 => [ 'pid' => '913', 'size' => '2', 'count' => 5, 'fee' => '3200' ],
];

// 1. 先用 array_values 得到值的列表(确保后续 JSON 编码为数组)
$valuesOnly = array_values($myArray);
// 2. 编码成 JSON 字符串
$jsonString = json_encode($valuesOnly);
// 3. 解码回 PHP 数组 (第二个参数 true 表示转为关联数组,但对纯数组没影响)
$reIndexedArray = json_decode($jsonString, true);

print_r($reIndexedArray);
// 输出结果仍然是正确的 0, 1, 2 索引数组
?>

但你看,这一通操作,核心还是第一步的 array_values()。直接用 json_decode(json_encode($myArray), true) 对于示例中的不连续数字索引数组,行为可能不总是得到 0-based 索引,有时可能会保留原key转成string key的对象。

所以,这种 JSON 方法通常不推荐用来解决单纯的索引重排问题。它更像是某些特殊场景下的 Workaround,或者是在数据需要在不同系统间(如 PHP 与 JavaScript)传递时的副产品。

优缺点

  • 优点: 无明显优点(针对此特定问题)。
  • 缺点: 绕!性能开销大(涉及序列化和反序列化)。可能引入意想不到的数据类型转换(如数字变字符串、对象变数组等)。代码意图极其不清晰。强烈不推荐 用于解决这个问题。

总结一下

当你因为 unset 或其他操作导致 PHP 数组的数字索引不再连续,需要将它们重新整理成从 0 开始的连续索引时:

  • 首选方案:array_values($array) 。简洁、高效、意图明确。它就是干这个的。
  • 备选方案:手动 foreach 循环构建新数组 $newArray[] = $value 。代码稍多,但逻辑清晰,便于理解,且可以在循环中添加自定义处理逻辑。
  • 可以但通常不推荐:array_splice($array, 0, 0) 。利用副作用实现,可读性差,并且直接修改原数组。
  • 强烈不推荐:JSON 编码解码技巧 。过于迂回,性能差,易出错。

根据你的具体需求和偏好,选择 array_values 或者手动循环通常是最好的选择。