前言array_slice()大概是「看一眼就会、用起来就错」的典型函数。它只有四个参数但每一个都有正负号、每一个都有边界情况叠在一起就成了一个小型的状态机。你很可能遇到过下面这些现象分页列表第一页少了第一条或者第二页重复了上一页的最后一条array_slice($arr, 1, -1)拿到的结果和「去掉首尾」的预期不一致偏移量明明超出了数组长度函数不报错也不警告直接返回空数组于是页面上只是「这一页空白」排查半天找不到原因用array_slice()截出一段之后丢给json_encode()前端收到的居然是{1:b,2:c}这样一个对象而不是数组传了个2.5或者null进去PHP 8 下冒出一条「隐式转换丢失精度」的废弃提示。这些问题的共同点是它们都不是 bug而是对参数的语义理解有偏差。array_slice()的行为被精确定义过只是那些定义散落在文档的各个角落。本文以PHP 8.5为环境把offset偏移量和length长度的全部分支一次讲清给出可直接照抄的边界表格、一个分页器实现以及九个真会踩的坑。示例代码最低要求PHP 8.0用到了构造函数属性提升与命名参数array_slice()本身的语义从 PHP 7 到 8.5 没有任何变化。一、函数签名与四条基本规则array_slice( array $array, int $offset, ?int $length null, bool $preserve_keys false ): array不用记「偏移量怎么算」这种模糊的说法直接记四条规则任何组合都能推出来$offset 0从下标$offset开始数组下标从 0 起。$offset 0从「倒数第|$offset|个元素」开始。注意-0就是0不是「从末尾数零个」。$offset超出数组范围返回空数组不报错、不警告。这是最容易被忽略的一条也是「沉默的 bug」的来源。$length省略或传null表示「一直到末尾」正数表示取这么多个负数表示「到距离末尾|$length|个元素为止」也就是把最后|$length|个丢掉。用$a [a, b, c, d, e];下标 0~4把常用组合全部列出来offsetlength结果说明0省略a b c d e全量拷贝1省略b c d e跳过第一个取到末尾12b c从下标 1 起取 2 个1-1b c d去掉首尾末尾丢掉 1 个0-2a b c去掉最后 2 个-2省略d e最后 2 个-21d倒数第 2 个-100省略a b c d e负数超出范围会从 0 开始不会返回空10省略空数组正向越界返回空静默1-10空数组length把区间吃完了请特别注意-100和10这两行负数偏移量越界会被「夹」到 0正向偏移量越界直接返回空数组。两者行为不对称这正是「有时候不报错、有时候拿到全部数据」这类诡异现象的来源。至于length 0结果永远是空数组——它是一个合法的「取零个」。二、偏移量的计算和后端分页公式完全一致业务代码里 90% 的array_slice()都用在分页上。分页的偏移量公式只有一条和 SQL 的LIMIT是同一套offset (page - 1) * perPage这里最容易错的是「页码从 1 开始、下标从 0 开始」这个错位。写成page * perPage就会永远跳过第一页而页面不会报任何错——你只会看到「第一页的数据不见了」然后怀疑数据源。页码从 1 起每页条数偏移量等价 SQL110(1-1)*10 0LIMIT 0, 10或LIMIT 10 OFFSET 0210(2-1)*10 10LIMIT 10 OFFSET 10310(3-1)*10 20LIMIT 10 OFFSET 20515(5-1)*15 60LIMIT 15 OFFSET 60页码是从 1 开始的人类习惯偏移量是从 0 开始的机器习惯中间减掉的这个 1 就是全部玄机。把公式封装进函数、永远不手写那个减法是唯一可靠的防御手段。顺便把容易和它混淆的array_splice()放在一起对比因为两者名字只差一个字母对比项array_slice()array_splice()是否修改原数组否返回新数组是按引用修改原数组返回值截取出来的那一段被移除的那一段length省略取到末尾删除到末尾键的处理可选择保留或重排整数键总是重排典型用途分页、取子集删除中间一段、插入替换一句话记忆slice 是「拿一段出来」splice 是「剪掉一段」。想删除元素却用了array_slice()会发现原数组纹丝不动返回值里反而是你以为删掉的内容。三、键的命运字符串键永远保留$preserve_keys这个参数的名字有误导性。它真正控制的是整数键$preserve_keys false默认整数键会被重新编号为0, 1, 2, ...$preserve_keys true整数键保持原样字符串键在任何情况下都被保留无论这个参数传什么。?php declare(strict_types1); $a [x 1, y 2, z 3]; var_dump(array_slice($a, 1)); // [y 2, z 3] —— 字符串键被保留了 $b [0 a, 1 b, 2 c]; print_r(array_slice($b, 1)); // [0 b, 1 c] —— 整数键被重排为 0,1 print_r(array_slice($b, 1, null, true)); // [1 b, 2 c] —— 整数键保持原样由此引出前面提到的那个高频坑preserve_keys true加上json_encode()数组会变成对象。?php $rows array_slice([alice, bob, carol], 1, 2, true); echo json_encode($rows), PHP_EOL; // {1:bob,2:carol} ← 是 JSON 对象 echo json_encode(array_values($rows)), PHP_EOL; // [bob,carol] ← 才是 JSON 数组原因是 PHP 的数组在序列化为 JSON 时会检查「键是否是从 0 开始、连续、整数」。只要有一个键不是整个数组就退化成 JSON 对象。前端拿到对象之后.map()就报错了而服务端日志里什么错都没有。四、完整可运行的分页器把上面的规则全部封装进函数业务代码就再也不会碰到这些边界。?php declare(strict_types1); // 最低 PHP 8.0 /** * 一个只读的分页视图对内存中的数组做切片。 * 适用于「数据量不大、已经在内存里」的场景读配置、读缓存、合并后的列表。 */ final class ArrayPaginator { /** var listmixed */ private array $items; public function __construct( array $items, private int $perPage 10, private int $page 1, ) { $this-items array_values($items); // 先正规化键的问题一次性解决 $this-perPage max(1, $perPage); // 每页至少 1 条避免除零 $this-page max(1, $this-page); // 页码从 1 起 } /** 核心偏移量只在这里算一次 */ public function offset(): int { return ($this-page - 1) * $this-perPage; } /** return listmixed */ public function items(): array { // 用 array_values 兜底即使以后有人改了 preserve_keys输出也是标准列表 return array_values( array_slice($this-items, $this-offset(), $this-perPage) ); } public function total(): int { return count($this-items); } public function totalPages(): int { return (int)ceil($this-total() / $this-perPage); } public function hasPrev(): bool { return $this-page 1; } public function hasNext(): bool { return $this-page $this-totalPages(); } /** 把越界页码夹回合法范围避免出现「空白页」 */ public function clamp(): self { $max max(1, $this-totalPages()); return new self($this-items, $this-perPage, min($this-page, $max)); } } // ----------------------------------------------------------- 验证 $data []; foreach (range(1, 23) as $i) { $data[] [id $i, name sprintf(user-%02d, $i)]; } $perPage 10; $totalPages (new ArrayPaginator($data, $perPage))-totalPages(); foreach (range(1, $totalPages) as $page) { $p new ArrayPaginator($data, $perPage, $page); $ids array_column($p-items(), id); printf( 第 %d/%d 页 偏移%2d 条数%2d id: %s\n, $p-page, $totalPages, $p-offset(), count($ids), implode(,, $ids) ); } // 边界页码越界 $over (new ArrayPaginator($data, $perPage, 99))-clamp(); printf(\n越界页码 99 夹回后: 第 %d 页, 条数 %d\n, $over-page, count($over-items())); // 边界空数组 $empty new ArrayPaginator([], $perPage, 1); printf(空数据: 总页数 %d, 当前条数 %d\n, $empty-totalPages(), count($empty-items()));运行输出第 1/3 页 偏移 0 条数10 id: 1,2,3,4,5,6,7,8,9,10 第 2/3 页 偏移10 条数10 id: 11,12,13,14,15,16,17,18,19,20 第 3/3 页 偏移20 条数 3 id: 21,22,23 越界页码 99 夹回后: 第 3 页, 条数 3 空数据: 总页数 0, 当前条数 0注意最后一页只有 3 条array_slice($data, 20, 10)在只剩 3 个元素时不会报错也不会补齐它老老实实返回 3 条。这正是我们想要的行为但如果你按「每页必须 10 条」去写校验逻辑就会误判。再看几个只跟「负数切片」有关的例子它们在与count()配合时最容易算错?php declare(strict_types1); $log [boot, connect, query, cache, shutdown]; // 最后 2 条 print_r(array_slice($log, -2)); // 去掉最后 1 条length 传 -1 比传 count()-1 更安全 print_r(array_slice($log, 0, -1)); // 去掉第一条和最后一条 print_r(array_slice($log, 1, -1)); // 倒数第 2 条只要 1 条 print_r(array_slice($log, -2, 1));输出Array ( [0] cache [1] shutdown ) Array ( [0] boot [1] connect [2] query [3] cache ) Array ( [0] connect [1] query [2] cache ) Array ( [0] cache )常见坑点❌ 分页偏移量写成$page * $perPage第一页被跳过✅ 永远是($page - 1) * $perPage把计算收进一个offset()方法业务代码不手写减法❌ 传null、或者2.5、1.5这类值当偏移量✅ PHP 8.1 起「隐式转换丢失精度」会触发Deprecatednull传给int参数同样被废弃一律在调用前(int)强转或者先校验❌array_slice($arr, 1, 2, true)之后直接json_encode()前端收到对象✅ 输出给前端的列表一律套一层array_values()保证键从 0 开始连续❌ 以为$preserve_keys false会把字符串键也重排✅ 字符串键永远保留重排的只有整数键想统一成列表就用array_values()❌ 用array_slice()想「删掉原数组中间一段」✅ 它不改原数组要改就用array_splice()并且记住它的返回值是被删掉的部分❌ 用array_slice($a, $offset, count($a) - $offset)算长度✅$offset为负时count() - $offset会算出偏大的值直接传null或省略length让函数自己算到末尾❌ 不校验偏移量正向越界时拿到空数组却以为「数据为空」✅ 正向越界静默返回空数组负数越界则夹到 0页码要先clamp到[1, totalPages]❌ 用array_slice()实现队列的pop或shift✅ 每次都全量拷贝复杂度是 O(n)队列场景用array_pop()/array_shift()或者干脆用SplQueue❌ 用array_slice()切一段后期待它「重新从 0 开始编号」但原数组里有字符串键✅ 混合键的数组切完键是不连续的需要连续下标就array_values(array_slice(...))一步到位总结场景正确写法关键点分页取第 N 页array_slice($a, ($page - 1) * $perPage, $perPage)页码 1 基、偏移 0 基取末尾 N 个array_slice($a, -N)负数偏移从末尾数去掉末尾 N 个array_slice($a, 0, -N)负数长度表示「丢末尾」去掉首尾array_slice($a, 1, -1)组合使用保证输出是 JSON 数组array_values(array_slice(...))键必须从 0 连续关联数组保序取子集array_slice($a, 1)字符串键天然保留修改原数组array_splice($a, $offset, $length)返回值是被移除的元素越界保护min($page, $totalPages)正向越界静默返回空偏移量的计算其实只有一条公式(页码 - 1) × 每页条数。真正需要额外留意的不是这行算术而是它周围的三个陷阱——正向越界静默返回空数组、字符串键永远保留、preserve_keys会让 JSON 数组退化成对象。把这个公式连同array_values()一起封进一个小类让业务代码只需要传页码这三类问题就都从源头消失了。