13-切片
19 分钟阅读
切片 (Slices)
面向 Rust 1.97.1 (stable, 2026-07)。本篇假设你熟悉 Go,凡有可比性处均给出 🐹 Go 对比。
热度:
hot高频 |common常见 |occasional偶尔 |advanced进阶少见
本篇能解决什么:
- 你是否分不清
&[T]、&str、数组和Vec,总把它们当成同一种“列表”? - 你是否写过
v[i]越界 panic,却不知道何时该改用get()? - 你是否想从
Vec<String>里v[0]拿走一个String,结果撞上error[E0507]? - 你是否疑惑:Go 的
[]T和 Rust 的切片长得很像,为什么生命周期和可变规则差这么多? - 你是否拿不准函数参数该写
&[T]还是Vec<T>?
术语速查表:
| 术语 / 缩略词 | 全称 / 读法 | 中文 | 一句话解释 | Go 里的近亲 |
|---|---|---|---|---|
| slice | — | 切片 | 对连续序列的借用视图,不拥有数据 | []T / string 头 |
| fat pointer | — | 胖指针 | 指针 + 长度(有时再加元数据)的宽指针 | slice header(ptr+len,无 cap) |
| DST | Dynamically Sized Type | 动态大小类型 | 编译期大小未知,必须通过指针使用 | 无直接对应 |
| UTF-8 | UTF-8 | UTF-8 编码 | 一个字符可能占 1~4 字节 | Go 的 string 也是 UTF-8 字节序列 |
| borrow | — | 借用 | 临时看或改,但不接管所有权 | 传指针 / 传切片头 |
| lifetime | — | 生命周期 | 引用允许存活的作用域上限 | 无编译期对应物 |
Deref | Dereference | 解引用转换 | 让 String/Vec 等“表现得像”切片 | 无直接对应 |
| owner | — | 所有者 | 负责这份值最终释放的人 | 无(Go 靠 GC) |
| GC | Garbage Collector | 垃圾回收器 | 运行时扫描并回收不用对象的机制 | Go 默认机制 |
| RAII | Resource Acquisition Is Initialization | 资源获取即初始化 | 资源跟着值的生命周期自动释放 | Go 里常要手写 defer |
Copy | — | 按位复制 trait | 赋值后原变量仍可用的小值类型 | Go 的普通值拷贝 |
Clone | — | 显式克隆 trait | 需要你手动调用的复制操作 | 手写深拷贝 |
说明:本表覆盖本篇出现的所有专业名词与缩略词;正文首次出现时仍会就地解释一次。
热度索引:
| 热度 | 题目 |
|---|---|
hot | Q1, Q2, Q3, Q4 |
common | Q5, Q6, Q7, Q8, Q9, Q10, Q11, Q15, Q16 |
occasional | Q12, Q13 |
advanced | Q14 |
Q1. &[T] 和 &str 本质上是什么?
Tags: hot beginner slice
适用版本: Rust 1.0+(1.97.1 行为一致)
一句话答案:
&[T] 是对连续 T 序列的借用视图;&str 是对一段保证有效 UTF-8(UTF-8 编码:一个字符可能占 1~4 字节)文本的借用视图——二者都不拥有底层数据。
解答:
切片(slice)不是“另一份列表”,而是对着已有内存开的一扇窗口:窗口里有起点和长度,真正的数据仍归数组、Vec 或 String 所有。
| |
同一套语法也能切 Vec / String——切的是借用,不是拷贝整份缓冲:
| |
可变切片改的是原数据,进一步证明“窗口”语义:
| |
&str 的底层是字节,但类型契约比 &[u8] 更严:内容必须始终是有效 UTF-8。字符串细节见 14-strings-and-text。
Go 对比:
| |
- Go 怎么做:
[]T/string也是对底层数组的窗口(再加 capacity 等字段)。 - Rust 为什么不同:Rust 把“窗口能活多久、能不能同时可变”交给借用检查器,而不是留给你和 GC。
- Go 程序员易踩的坑:别把切片当成“复制了一份元素”;两边改的都是同一块底层内存。
记忆点:
&[T]/&str= 借用视图,不拥有数据。- 改可变切片 = 改源数据。
&str额外保证 UTF-8 有效。
Q2. 为什么切片不拥有数据?
Tags: hot beginner slice ownership
适用版本: Rust 1.0+
一句话答案:
因为切片的设计目标是“零成本偷看一段连续内存”;所有权仍在源数据上,切片离开作用域时只丢掉窗口,不会释放底层缓冲。
解答:
若 &[T] 也拥有数据,那每次取子区间都要拷贝或转移所有权,API 会又慢又难用。Rust 的选择是:所有者负责释放(RAII:Resource Acquisition Is Initialization,资源获取即初始化),切片只借用。
| |
正因为不拥有,切片不能独自“活过”源数据——源数据被 drop 后,窗口会变成悬垂引用,编译器直接禁止:
| |
需要真正拥有一段切片数据时,用 Vec<T> / String,或 Box<[T]> / Box<str>(堆上拥有的切片),而不是指望 &[T] 自己保管内存。
| |
Go 对比:
| |
- Go 怎么做:slice 头也不拥有“独占释放权”,底层数组何时回收交给 GC(Garbage Collector,垃圾回收器)。
- Rust 为什么不同:没有 GC 时,必须把“谁释放”钉死在所有者上;切片只能借。
- Go 程序员易踩的坑:在 Rust 里返回指向局部
Vec的切片,会直接编译失败——这是特性,不是刁难。
记忆点:
- 切片 = 窗口;所有者 = 负责释放的人。
- 窗口结束 ≠ 释放缓冲。
- 要拥有 →
Vec/String/Box<[T]>。
Q3. 索引和 get() 该怎么选?
Tags: hot beginner indexing
适用版本: Rust 1.0+
一句话答案:
s[i] / s[a..b] 在越界(或对 &str 切到非字符边界)时 panic;get / get_mut 返回 Option,失败是 None——库代码和不可信输入优先 get。
解答:
索引写法短,但契约是“调用者保证合法”。越界会直接崩:
| |
对 &str 还有 UTF-8 字符边界问题:索引按字节,切到多字节字符中间会 panic;get(range) 则返回 None:
| |
确定合法、且在热路径上,才考虑索引;不确定时用 get,或先用 is_char_boundary / char_indices 找安全切点:
| |
Go 对比:
| |
- Go 怎么做:下标越界同样 panic;字符串切片也是字节级。
- Rust 为什么不同:Rust 用类型系统保证
&str永远是有效 UTF-8,所以非法字符边界直接拒绝。 - Go 程序员易踩的坑:把
len(s)当“字符数”,再按字符数去切&str,极易 panic。
记忆点:
- 越界索引 → panic;
get→None。 &str必须切在字符边界,否则 panic;get更安全。- 不可信输入优先
get/chars。
Q4. 为什么不能从切片索引里 move 出 String?
Tags: hot beginner move E0507
适用版本: Rust 1.0+
一句话答案:
v[i] 对非 Copy 元素只能给出借用;若允许 let s = v[0] 把 String move 走,会在 Vec 里留下“空洞”,破坏容器不变量——因此编译器报 error[E0507]。
解答:
i32 实现了 Copy,索引看起来像“取值”;String 没有 Copy,索引不能搬走:
| |
「✅ 正确写法」——需要所有权时,用会真正取出元素的 API,或显式克隆:
| |
切片本身也一样:&[String] 上的索引更不可能 move,因为你连容器所有权都没有:
| |
Go 对比:
| |
- Go 怎么做:赋值通常复制值头;没有“从切片 move 走所有权”的编译期概念。
- Rust 为什么不同:
String有唯一所有者;随意 move 出会让Vec无法安全 drop 剩余元素。 - Go 程序员易踩的坑:看到
v[i]就以为能拿到所有权;在 Rust 里先想“借、clone,还是remove/swap_remove/pop”。
记忆点:
E0507= 不能从索引 move 出非Copy值。- 借用 /
clone/remove(等)三选一。 - 切片上更不可能 move——你只是访客。
Q5. String / Vec 为什么能自动变成切片?
Tags: common beginner deref
适用版本: Rust 1.0+
一句话答案:
因为 String 实现了 Deref<Target = str>,Vec<T> 实现了 Deref<Target = [T]>——在需要 &str / &[T] 的地方,编译器会自动做 Deref(Dereference,解引用转换)强制。
解答:
这就是为什么 API 参数写 &str / &[T] 最友好:调用方可以传字面量、String、Vec、数组引用:
| |
数组也一样,&[T; N] 可以强制成 &[T]:
| |
Go 对比:
| |
- Go 怎么做:数组到 slice 通常要写
a[:];string与[]byte转换常显式拷贝或转换。 - Rust 为什么不同:
Deref让“拥有型容器”在只读 API 里表现得像切片,减少样板代码。 - Go 程序员易踩的坑:以为
&String和&str是两种完全不能互通的类型——多数只读场景可以直接传&s。
记忆点:
- 参数优先
&str/&[T]。 &String/&Vec<T>常自动变成切片引用。- 需要所有权时再收
String/Vec<T>。
Q6. 切片是不是 DST(动态大小类型)?
Tags: common intermediate dst
适用版本: Rust 1.0+
一句话答案:
是。[T] 与 str 都是 DST(Dynamically Sized Type,动态大小类型:编译期大小未知);你日常写的 &[T] / &str 是指向它们的胖指针(fat pointer:数据指针 + 长度)。
解答:
普通引用 &i32 是一个指针宽;切片引用要额外记住“这段有多长”,所以通常是两个字宽:
| |
正因为 [T] / str 大小未知,不能按值放在变量里或直接作为函数返回类型(除非藏在指针后面):
| |
Box<[T]>、Box<str> 同样是胖指针,但拥有堆上数据——这是“拥有的切片”,不是借用窗口。
Go 对比:
| |
- Go 怎么做:
[]T头固定大小(含 cap);没有“无大小的[T]类型”这种语言层 DST。 - Rust 为什么不同:把“变长序列本身”建模成 DST,再用胖指针引用,类型更精确。
- Go 程序员易踩的坑:把
&[T]想成 Go 的三字段 slice;Rust 借用切片通常只有 ptr+len,cap 在Vec上。
记忆点:
[T]/str= DST;&[T]/&str= 胖指针。- 胖指针 ≈ 指针 + 长度。
- 不能把 DST 当普通栈变量“裸拿”。
Q7. 范围语法 a[..] 到底有哪些变体?
Tags: common beginner range
适用版本: Rust 1.0+;..= 包容范围为稳定特性
一句话答案:
常用的是 start..end(不含 end)、start..=end(含 end),以及省略一端的 ..end、start..、..(全切片)。
解答:
| |
对 str 同样用范围,但边界必须是 UTF-8 字符边界,否则与 Q3 一样会 panic;不确定时用 get:
| |
Go 对比:
| |
- Go 怎么做:
low:high一律半开;靠调整 high 表达“含尾”。 - Rust 为什么不同:额外提供
..=,意图更直白。 - Go 程序员易踩的坑:把 Go 的
a[1:3]写成 Rusta[1..=3],会多切一个元素。
记忆点:
..半开;..=含尾。- 可省略 start / end / 两端。
&str范围仍受 UTF-8 边界约束。
Q8. 为什么 split_at_mut 能安全拿两段可变切片?
Tags: common intermediate split_at_mut
适用版本: Rust 1.0+
一句话答案:
因为标准库保证两段不重叠;不重叠就没有别名可变借用,满足借用规则——这是 12-references-and-borrowing 里的经典解法。
解答:
「❌ 错误写法」——对同一数组直接取两个 &mut:
| |
「✅ 正确写法」——split_at_mut 一次切开:
| |
编译器自己推不出“这两个下标不重叠”,但 split_at_mut 的实现用 unsafe 证明了这一点,并把安全接口暴露给你。需要多个离散位置时,可用 get_disjoint_mut(1.86+)。
Go 对比:
| |
- Go 怎么做:本来就可以把一个 slice 切成两段再分别改;语言不静态检查重叠。
- Rust 为什么不同:默认禁止两个
&mut;只有证明不重叠的 API 才能放行。 - Go 程序员易踩的坑:下意识写两个
&mut a[i],在 Rust 里会被硬拦。
记忆点:
- 两个
&mut默认不行。 split_at_mut= 安全的不重叠分割。- 重叠别名可变 = 数据竞争温床。
Q9. &str 为什么既像字符串又像切片?
Tags: common beginner str utf8
适用版本: Rust 1.0+
一句话答案:
因为 &str 在形态上就是“字符串版的 &[u8]”:同样是胖指针窗口;但它额外承诺内容是有效 UTF-8,并提供文本向的方法。
解答:
字面量 "hi" 的类型是 &'static str;String 解引用后也能得到 &str:
| |
你可以把它当成字节切片看,但要回到 &str 必须再次验证 UTF-8:
| |
索引、范围、生命周期规则与普通切片一致,只是多了字符边界约束;更完整的字符串故事见 14-strings-and-text。
Go 对比:
| |
- Go 怎么做:
string与[]byte是近亲,转换常见且语义偏“值头/字节序列”。 - Rust 为什么不同:
&str与&[u8]类型分开,用 UTF-8 不变量换取更安全的文本 API。 - Go 程序员易踩的坑:把
&str当成可改的[]byte;要改文本通常应使用String。
记忆点:
&str≈ 保证 UTF-8 的字符串切片。- 像切片一样借;像字符串一样用文本 API。
- 可变文本 →
String,只读视图 →&str。
Q10. 切片生命周期为什么总是跟源数据绑在一起?
Tags: common lifetime borrow
适用版本: Rust 1.0+
一句话答案:
因为切片是借用:它的 lifetime(生命周期:引用允许存活的作用域上限)不能超过源数据,否则会变成悬垂指针。
解答:
子切片的生命周期标注会跟输入走:
| |
从 Vec 取出的切片,在可能触发重分配的 &mut Vec 操作面前会失效——借用检查器会挡住 use-after-realloc:
| |
想让切片“独立活下去”,只能拷贝出拥有型数据(to_vec / to_string),不能延长借用。
Go 对比:
| |
- Go 怎么做:slice 头可随便返回;悬垂问题靠 GC 与程序员纪律缓解。
- Rust 为什么不同:生命周期把“窗口不能长过房子”写成类型的一部分。
- Go 程序员易踩的坑:从函数返回指向局部数组的切片——在 Rust 里直接
E0515/ 生命周期错误。
记忆点:
- 切片寿命 ≤ 源数据寿命。
- 持有切片时,别对源做可能失效借用的可变操作。
- 要独立副本 →
to_vec/to_string。
Q11. 什么时候参数应该写成 &[T] 而不是 Vec<T>?
Tags: common api slice
适用版本: Rust 1.0+
一句话答案:
只要函数只读(或通过 &mut [T] 就地改)元素、不需要拿走所有权或增长缓冲,就写 &[T] / &mut [T];只有要拥有、要 push/append 时才收 Vec<T>。
解答:
&[T] 让数组、Vec、子切片都能传入,调用成本更低:
| |
「❌ 不必要的所有权」——只求和却吃掉 Vec,调用方每次都要重建或 clone:
| |
字符串同理:只读文本用 &str,要拥有再用 String。
Go 对比:
| |
- Go 怎么做:只读函数几乎总是收
[]T,很少有人传“拥有型容器类型”这种区分。 - Rust 为什么不同:
Vec<T>参数意味着可能 move;&[T]明确“只借用不接管”。 - Go 程序员易踩的坑:照着自己的结构体字段类型把参数写成
Vec<T>,结果把调用方数据吃掉。
记忆点:
- 只看 / 就地改 →
&[T]/&mut [T]。 - 要拥有或扩容 →
Vec<T>。 - 文本同理:
&strvsString。
Q12. Go 的 slice 和 Rust 的 slice 最像,但差在哪?
Tags: occasional beginner go
适用版本: Rust 1.0+
一句话答案:
形态上都是“指向连续内存的窗口”;差别在于 Rust 切片通常无 cap、强绑定生命周期与借用规则,且 &str 有 UTF-8 不变量。
解答:
先看并排直觉:两边都能从已有序列切出子区间并共享底层存储:
| |
关键差异可以记三张卡片:
- 头字段:Go
[]T常有 ptr+len+cap;Rust&[T]通常只有 ptr+len,cap 在Vec。 - 别名规则:Go 允许多个 slice 同时改重叠区域(数据竞争靠你);Rust 禁止重叠
&mut。 - 寿命:Go 靠 GC;Rust 切片不能活过源数据。
| |
Go 对比:
| |
- Go 怎么做:slice 是日常一等公民,append 可能换底层数组,别名问题运行时自行小心。
- Rust 为什么不同:把共享/可变/寿命前移到编译期,换取无数据竞争的默认安全。
- Go 程序员易踩的坑:把 Rust
&mut [T]当成可以随意重叠再切的 Go slice。
记忆点:
- 像:都是窗口,共享底层。
- 不像:cap、借用规则、生命周期、UTF-8。
- 迁移口诀:先借后改,别假设能乱别名。
Q13. 数组、切片、Vec 到底怎么分工?
Tags: occasional beginner array vec
适用版本: Rust 1.0+
一句话答案:
[T; N] 固定长度、通常栈上拥有;[T] / &[T] 是动态长度视图;Vec<T> 是可增长的堆上所有者——三者常通过切片 API 互通。
解答:
| |
选型和参数传递的经验法则:
| |
| 类型 | 大小 | 拥有? | 典型场景 |
|---|---|---|---|
[T; N] | 固定 N | 是 | 小缓冲、长度是类型的一部分 |
&[T] | 胖指针 | 否 | API 只读/就地改 |
Vec<T> | 指针+len+cap | 是 | 运行期增长 |
Go 对比:
| |
- Go 怎么做:数组少用,日常几乎都是
[]T。 - Rust 为什么不同:把“固定长 / 借用视图 / 可增长拥有”拆开,避免一个类型扛所有语义。
- Go 程序员易踩的坑:把 Rust 数组当成 Go 数组那样很少用——在 Rust 里
[T; N]很常见且零成本。
记忆点:
- 固定长 → 数组;窗口 → 切片;可增长 →
Vec。 - API 默认收
&[T]。 - 需要 cap/push 才上
Vec。
Q14. 本章最值得背的切片规则是什么?
Tags: advanced summary slice
适用版本: Rust 1.0+
一句话答案:
切片是借用窗口:不拥有、寿命绑源数据、索引可能 panic、非 Copy 不能从索引 move,API 优先 &[T] / &str。
解答:
把前面各题收成一张可背清单,并配两个最小例子钉牢:
| |
| |
背诵版:
&[T]/&str= 窗口,不是所有者。- 胖指针 = 指针 + 长度;
[T]/str是 DST。 - 越界索引 / UTF-8 半字符切片 → panic;
get→None。 - 索引不能 move 出
String等非Copy(E0507)。 - 参数优先
&[T]/&str;split_at_mut解决两段可变。 - 寿命 ≤ 源数据;要独立副本就
to_vec/to_string。
Go 对比:
| |
- Go 怎么做:
[]T几乎通吃;越界仍 panic,但没有 move/E0507这套。 - Rust 为什么不同:切片规则是所有权与借用在“连续内存视图”上的具体化。
- Go 程序员易踩的坑:只记住“和 Go slice 很像”,却忘了生命周期与 move 规则。
记忆点:
- 窗口、不拥有、寿命绑定。
- panic vs
None;E0507不能索引 move。 - API 写
&[T]/&str,需要时再Vec/String。
Q15. 切片怎么用 chunks / windows 切块?
Tags: common beginner chunks windows
适用版本: Rust 1.0+
一句话答案:
chunks(n) / chunks_mut(n) 把切片切成不重叠的长度至多为 n 的块;windows(n) 给出长度恰好为 n 的滑动窗口(相邻窗口重叠)。
解答:
按固定大小分批处理时用 chunks:最后一块可以短于 n。只要完整块、剩余单独处理,用 chunks_exact:
| |
需要看“当前元素和邻居”时用 windows:每一步只前进 1,窗口互相重叠:
| |
可变分块用 chunks_mut(窗口没有通用的 windows_mut,因为重叠可变借用不安全):
| |
Go 对比:
| |
- Go 怎么做:通常手写步长循环,或自己封装切块函数。
- Rust 为什么不同:标准库把“不重叠分块”和“滑动窗口”做成迭代器 API,少写边界算术。
- Go 程序员易踩的坑:把
windows(n)当成chunks(n),结果块数变多且内容重叠。
记忆点:
chunks= 不重叠切块;末块可能更短。windows= 滑动重叠窗口。- 要改元素 →
chunks_mut,别指望重叠的可变windows。
Q16. copy_from_slice 怎么安全拷贝?
Tags: common beginner copy_from_slice
适用版本: Rust 1.0+
一句话答案:
dst.copy_from_slice(src) 要求两边长度相等且元素实现 Copy,一次把 src 拷进 dst;长度不匹配会 panic——先比 len,或改用等长的子切片。
解答:
最常见用法:准备好同样长的目标缓冲,再拷:
| |
只覆盖一部分时,先切出等长窗口再拷——这比“整片长度碰巧对上”更清晰:
| |
「❌ 错误写法」——长度不等会直接 panic(不是返回 Result):
| |
元素没有 Copy 时,用 clone_from_slice(需要 Clone),或逐个赋值 / to_vec 之类显式策略。不确定长度时,先 assert_eq!(dst.len(), src.len()),或只对 dst.get_mut(..src.len()) 这类已核对过的窗口操作。
Go 对比:
| |
- Go 怎么做:内建
copy按较短那边的长度拷,返回实际拷贝个数。 - Rust 为什么不同:
copy_from_slice契约更严——等长才拷,避免静默截断。 - Go 程序员易踩的坑:照 Go 习惯假定“多出来的自动丢掉”;在 Rust 里长度不对会直接崩。
记忆点:
- 等长 +
Copy→copy_from_slice。 - 不等长 → panic;先切齐或先检查
len。 - 非
Copy→clone_from_slice(或别的显式拷贝)。