切片:为函数设计稳定、低耦合的输入
课程概览 · 第 3 章
上一章的结论是"只读文本用 &str"。本章把同一思路推广到任意序列:函数应该声明自己需要的是"一段数据"还是"修改一段数据",而不是绑死调用方用 Vec 还是数组。切片就是承载这个契约的类型;边界与 UTF-8 是它真正的坑所在。
学习目标与默认选择
学完本章你能:
- 用
&[T]/&mut [T]/&str写出不绑定具体容器的函数; - 在可信内部索引与用户可控范围之间选择
[]或get; - 区分数值越界与 UTF-8 字符边界违反这两类
get失败; - 解释切片为什么"借用而不分配"。
默认选择:
| 函数需要 | 签名 | 调用方付出什么 |
|---|---|---|
| 只读一段序列 | &[T] | 一次借用,函数返回后数据照常使用 |
| 修改一段序列 | &mut [T] | 借用期间独占,返回后照常使用 |
| 拿走序列并保存 | Vec<T> / T | 转移所有权(本章不默认) |
| 只读文本 | &str | 同 &[T],且保证合法 UTF-8 |
切片接口:读、改、取前缀
一个程序演示三种切片接口:average 只读并对空输入返回 None;zero_out 独占修改;first_two 对不足两个元素的输入返回 None 而不是 panic。
1 | |
first_two(&[0xFF]) 返回 None 是本章的关键断言:get(..2) 在范围越界时给你一个可分支处理的 Option,而不是让程序崩溃。用户可控的长度/下标,一律走 get。
边界:直接索引 vs get
a[start..end] 越界会 panic;a.get(range) 越界返回 None。两者的分工不是"谁更安全",而是错误的性质不同:
- 边界由代码内部逻辑保证(循环变量来自
len()、切分点来自char_indices()):直接[]/范围即可,panic 意味着内部逻辑有 bug,崩溃是正确行为; - 边界来自外部输入(用户输入的长度、网络包里声明的字节数):必须
get,把"不合法"作为正常业务分支处理。
1 | |
范围语法速查:
| 写法 | 范围 | 典型用途 |
|---|---|---|
a[start..end] | start <= i < end | 最常见,长度是 end - start |
a[start..] | 到结尾 | 跳过前缀 |
a[..end] | 从开头 | 取前缀 |
a.get(start..end) | 返回 Option | 外部输入或边界不可信时 |
UTF-8 边界:同一条规则的两面
第 2 章已经给出字符串切片的完整规则:切分点必须落在字符边界上,[] 违反时 panic、get 返回 None。这里只需要把那条规则放回切片语境,理解它和数值越界是同一机制:
1 | |
&str 与 &[u8] 的差别只在最后那层检查:&[T] 检查数值边界,&str 在此之上还检查字符边界。按字符截断的 char_indices() 写法已在第 2 章展开,本章不再重复。
借用而不分配
&[T] 和 &str 是胖指针(地址 + 长度),传递它们只是复制两个机器字,不复制元素。只有 to_vec()、to_string()、collect() 这类操作才会分配新的拥有者:
1 | |
为什么可行:切片是胖指针
&[T] 概念上是(数据起始地址, 元素数量);&str 是(字节地址, 字节长度)。因此传切片是固定大小的复制,元素不搬家;长度随引用同行,编译器据此在索引处插入边界检查,并能在证明安全时优化掉。&str 额外携带"内容是合法 UTF-8"的保证,所以边界检查之外还有字符边界检查。get 与 [] 的差异只是越界时的行为(None vs panic),检查本身两者都做。
常见误区
- 函数签名绑死容器:写
fn f(v: Vec<i32>)或fn f(v: &Vec<i32>),调用方被迫分配/让接口无法接受数组;只要读,就写&[i32]。 - 把
get的None当异常:用户可控范围下"越界"是正常输入的一种,走业务分支,不要 unwrap。 - 用字节下标切中文文本:
&title[..2]可能 panic 或get出None;按字符截断先char_indices()。 - 为了"安全"全部改用
get+ unwrap:values.get(i).unwrap()比直接values[i]更难读;内部可信索引用[],让 bug 崩溃得响亮。 - 在切片上调用
to_vec()只为了遍历:切片迭代零分配,复制了才需要to_vec()。
自测
first_two(&[0xAB])返回什么,为什么不是 panic?–答方向:None;get(..2)把范围不足表达为Option,让调用方分支处理。- 什么情况下用
a[i..j],什么情况下必须a.get(i..j)?–答方向:边界由内部逻辑保证用前者(panic 暴露 bug);边界来自外部输入用后者(越界是业务输入)。 "修".get(..2)与[0xE4, 0xBF, 0xAE].get(..2)一个返回None一个返回Some,差别在哪?–答方向:&str除数值边界外还检查字符边界(2 落在"修"的 3 字节中间);&[u8]只检查数值边界。- 传递一个 100 万元素的切片,复制了多少数据?–答方向:两个机器字(指针 + 长度);元素不复制。
average(&[])返回None而zero_out(&mut [])照常返回,两个设计为什么都合理?–答方向:"没有平均值"是业务上有意义的结果缺失,用Option;"清零零个元素"没有失败含义,正常完成。






