课程概览 · 第 3 章

上一篇:字符串与切片:String、&str 与 UTF-8 边界

下一篇:表达式与控制流:让分支、循环和返回值清楚可审查

固定 4 字节协议头、运行期增长的任务列表和只读取其中一段的函数,需要不同类型。数组 [T; N] 拥有固定数量的元素,切片引用 &[T] / &mut [T] 借用连续数据,范围描述要选取的区间。本章沿这个顺序设计接口,再处理数值边界与 UTF-8 边界。

切片读写权限、连续视图与数值和 UTF-8 边界
图:读写权限、输入范围是否经过验证、是否为 UTF-8 文本,共同决定接口选择。

学习目标

前置知识:第 1 章的引用、Option / Result,第 2 章的字符串边界。学完你能:

  • 初始化并遍历数组,判断复制、借用与移动;
  • 用数组引用保留固定长度,用切片放宽长度要求;
  • 设计只读、修改和取前缀接口,覆盖空输入与短输入;
  • 按范围来源选择索引或 get,预测半开与含端点边界。

本章独立示例只用标准库,采用 Edition 2024;每块单独写入 binary 项目的 src/main.rs,运行 cargo +stable run。迭代器链先作为标准库操作使用,第 12 章再解释适配器与消费者。

数组:声明、初始化与所有权

数组类型写作 [T; N],元素同类型,长度是编译期常量。因此 [u8; 4] 与 [u8; 8] 是不同类型;mut 允许修改元素或换成同类型的新数组,不允许改变长度。运行期决定数量或需要增删时选择 Vec<T>,没有必要增长时不必为此分配。数组类型

独立示例 03-1(标准库;文件:src/main.rs)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
fn main() {
let delays: [u32; 3] = [1, 5, 10];
assert_eq!(delays.len(), 3);
assert_eq!(delays[0], 1);
let mut buffer = [0u8; 4];
buffer[1] = 7;
assert_eq!(buffer, [0, 7, 0, 0]);
assert_eq!(buffer.get(4), None);
let empty: [u8; 0] = [];
assert!(empty.is_empty());
let grid: [[i32; 2]; 2] = [[1, 2], [3, 4]];
assert_eq!(grid[1][0], 3);

let mut tasks: [String; 3] = std::array::from_fn(|i| {
format!("task-{}", i + 1)
});
tasks[0].push_str("-ready");
assert_eq!(tasks[0], "task-1-ready");
assert_eq!(tasks[1], "task-2");
let blank: [String; 3] = [const { String::new() }; 3];
assert!(blank.iter().all(String::is_empty));
}

普通 [value; N] 在重复多个元素时求值一次,再要求结果实现 Copy;[String::from("task"); 3] 不会自动调用 clone,也不能编译。from_fn 为每个下标单独构造元素;常量项和 const { ... } 块有专门的逐元素求值规则。数组表达式

数组在 T: Copy 时也是 Copy;[String; N] 按值使用会移动。共享遍历得到 &T,可变遍历得到 &mut T,按值遍历得到 T。Edition 2024 的 array.into_iter() 按值迭代,旧 Edition 2015/2018 的方法调用有兼容行为。

独立示例 03-2(标准库;文件:src/main.rs)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
fn main() {
let numbers = [1, 2, 3];
let copied = numbers;
assert_eq!(numbers, copied);

let mut tasks = [String::from("write"), String::from("test")];
for (index, task) in tasks.iter().enumerate() {
assert!(!task.is_empty());
assert!(index < tasks.len());
}
for task in &mut tasks {
task.push_str("-ready");
}
assert_eq!(tasks[0], "write-ready");
let mut total_len = 0;
for task in tasks {
total_len += task.len();
}
assert_eq!(total_len, 21);
}

不能直接用 let first = tasks[0]; 从数组里搬走非 Copy 元素,否则留下未初始化位置;只读用 &tasks[0],拿走全部元素可以按值遍历或解构数组。这里的所有权规则与其他值一致,第 6 章展开。

数组引用到切片

类型是否拥有元素长度契约
[T; N]是长度在类型中
&[T; N]否借用完整固定长度数组
[T]本身不规定所有者动态大小类型,通常经引用或 Box 使用
&[T] / &mut [T]否引用携带运行期长度,不能增删元素
Vec<T>是运行期可增长或缩短

&array 可以在参数位置转换为 &[T];&array[..] 或 array.as_slice() 明确写出切片。转换不复制元素。只处理序列时,&[T] 比 &Vec<T> 更少绑定容器选择。

独立示例 03-3(标准库;文件:src/main.rs)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
fn decode_id(bytes: &[u8; 4]) -> u32 {
u32::from_be_bytes(*bytes)
}

fn sum(values: &[u8]) -> u32 {
values.iter().map(|&value| u32::from(value)).sum()
}

fn main() {
let header = [0u8, 0, 0, 42];
assert_eq!(decode_id(&header), 42);
let values = [10u8, 20, 30];
let dynamic = vec![10u8, 20, 30, 40];
assert_eq!(sum(&values), 60);
assert_eq!(sum(&dynamic), 100);
let tail: &[u8] = &values[1..];
assert_eq!(tail, &[20, 30]);
assert_eq!(sum(tail), 50);
}

反向转换切片为数组引用需要检查长度。下面先用 get(..4) 处理短包,再显式匹配 try_into() 的结果;此时暂不引入第 5 章的 ?。

独立示例 03-4:协议头主例(标准库;文件:src/main.rs;TryInto 由 prelude 提供)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
fn read_id(packet: &[u8]) -> Option<u32> {
let prefix = match packet.get(..4) {
Some(bytes) => bytes,
None => return None,
};
let header: &[u8; 4] = match prefix.try_into() {
Ok(bytes) => bytes,
Err(_) => return None,
};
Some(u32::from_be_bytes(*header))
}

fn main() {
assert_eq!(read_id(&[0, 0, 0, 42, 9]), Some(42));
assert_eq!(read_id(&[0, 0, 42]), None);
assert_eq!(read_id(&[]), None);
let short: &[u8] = &[1, 2, 3];
let result: Result<&[u8; 4], _> = short.try_into();
assert!(result.is_err());
}

&[u8; 4] 借用原数据,*header 复制四个 u8 给大端解码。转换目标若为 [u8; 4],则直接复制元素得到新数组。这里的前缀长度已经是 4,转换失败分支仅完整表达转换 API 的合同,不把 unwrap 当作外部输入校验。任意固定长度的泛化见第 10 章。

只读、修改与取前缀接口

average 对空输入返回 None,不用 0.0 冒充平均值;zero_out 修改借用范围;first_two 对不足两个字节的输入返回 None。

独立示例 03-5(标准库;文件:src/main.rs)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
fn average(values: &[f64]) -> Option<f64> {
if values.is_empty() {
return None;
}
Some(values.iter().sum::<f64>() / values.len() as f64)
}

fn zero_out(values: &mut [i32]) {
values.fill(0);
}

fn first_two(input: &[u8]) -> Option<&[u8]> {
input.get(..2)
}

fn main() {
let values = [10.0, 20.0, 30.0, 40.0];
assert_eq!(average(&values), Some(25.0));
assert_eq!(average(&values[1..3]), Some(25.0));
assert_eq!(average(&[]), None);
let mut ints = [10, 20, 30, 40];
zero_out(&mut ints[2..]);
assert_eq!(ints, [10, 20, 0, 0]);
zero_out(&mut []);
assert_eq!(first_two(&[1, 2, 3]), Some(&[1, 2][..]));
assert_eq!(first_two(&[0xFF]), None);
assert_eq!(first_two(&[]), None);
}

外部未验证下标优先 get;内部已经证明边界时可以索引。不要为“安全”改成 get(...).unwrap(),那仍会 panic,反而掩盖为何边界成立。

索引与 get

数值索引要在长度内,范围索引要满足起终点顺序与长度约束。索引违反合同会 panic,get 返回 None。经过一次输入验证的下标也可以在已证明范围中使用索引,不是所有来自外部的值永远只能 get。

独立示例 03-6(标准库;文件:src/main.rs)。

1
2
3
4
5
6
7
8
9
10
11
12
fn main() {
let values = [30, 45, 60, 90];
assert_eq!(&values[1..3], &[45, 60]);
match values.get(1..3) {
Some(window) => assert_eq!(window, &[45, 60]),
None => panic!("known range must exist"),
}
assert_eq!(values.get(4), None);
assert!(values.get(3..10).is_none());
let (start, end) = (2, 1);
assert!(values.get(start..end).is_none());
}

故意失败:运行期 panic 03-7(标准库;文件:src/main.rs;通过参数传入越界下标,避免字面量越界先触发编译诊断)。

1
2
3
4
5
6
7
fn direct_index(values: &[i32], index: usize) -> i32 {
values[index]
}

fn main() {
let _ = direct_index(&[10, 20], 2);
}

范围边界

Edition 2024 的 a..b 构造 std::ops::Range,范围是可传参的普通值,不只是索引记号。下表迭代能力针对整数端点;Range<f64> 可判断包含关系,但不能直接迭代。

写法类型语义整数迭代
a..bRange含起点,不含终点可
a..=bRangeInclusive含两端可
a..RangeFrom有起点,无上界可,需自行停止
..bRangeTo从序列开头,不含终点否
..=bRangeToInclusive从序列开头,含终点否
..RangeFull整段否

半开区间的终点允许等于 len;普通非空含端点区间要求终点小于 len。到结尾直接写 1..,整段写 ..,不要用 ..=len-1:空序列会发生减法溢出,也不必手工对外部终点加一。

独立示例 03-8(标准库;文件:src/main.rs)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
use std::ops::Range;

fn count_in(tasks: &[&str], window: Range<usize>) -> Option<usize> {
match tasks.get(window) {
Some(slice) => Some(slice.len()),
None => None,
}
}

fn main() {
let tasks = ["write", "review", "test", "ship"];
let window = 2..4;
assert_eq!(count_in(&tasks, window.clone()), Some(2));
assert_eq!(&tasks[window], &["test", "ship"]);
assert_eq!(count_in(&tasks, 3..99), None);
let values = [10, 20, 30];
assert_eq!(&values[1..=2], &[20, 30]);
assert!(values.get(0..=3).is_none());
assert!(values.get(3..=3).is_none());
assert_eq!(&values[1..], &[20, 30]);
assert!([0, 1, 2][2..2].is_empty());
assert_eq!((1..4).sum::<i32>(), 6);
assert_eq!((1..=4).sum::<i32>(), 10);
assert_eq!((5..5).count(), 0);
assert!((60..=100).contains(&87));
}

Range 的 start、end 是公有字段;RangeInclusive 用 .start()、.end() 读取端点。ops::Range 是 Clone 而不是 Copy,复用时显式克隆。无上界范围仍受整数表示上限约束,不表示永远不溢出。遍历元素优先 for item in &values,同时需要位置时用 .enumerate();真正需要下标区间才使用 0..len()。

match 中的 0..=59 是范围模式,不构造范围值,端点必须满足模式规则而不能是普通运行期变量。第 4、9 章展开模式覆盖;汉字区间也不能当作所有中文字符的完整判定。

UTF-8 对照

普通切片只约束数值范围;字符串还要求字符边界,str 不等同于 [u8]。第 2 章的 char_indices 先生成合法字节位置,这里不重复截断算法。

独立示例 03-9(标准库;文件:src/main.rs)。

1
2
3
4
5
6
7
8
fn main() {
let title = "修复 login";
assert!(title.get(..100).is_none());
assert_eq!(title.get(..2), None);
assert_eq!(title.get(..3), Some("修"));
let bytes = [0xE4u8, 0xBF, 0xAE];
assert_eq!(bytes.get(..2), Some(&[0xE4, 0xBF][..]));
}

深入阅读

连续存储与借用布局

数组连续存放 N 个元素,大小为 N * size_of::<T>(),不需要另存运行期长度。位置由所有者决定:结构体字段随结构体存放,Box<[T; N]> 的数组由堆分配拥有,局部大数组可能占用较大栈空间,不能把数组等同于栈内存。[String; 3] 连续存放三个描述符,不把三段文本合并为一个缓冲区。数组布局

&[T] 概念上携带地址与元素数,&str 携带地址与字节数;共享引用传递不复制元素。&mut [T] 也携带长度,但不能像共享引用一样随意复制。借用的有效期与访问权限受所有者约束,见第 6 章。切片迭代不需要先 to_vec();需要独立拥有才复制。to_vec、collect 是否分配还取决于元素和目标类型,例如零大小类型有不同的容量行为。

范围迭代器保存进度

ops::Range 既描述区间又保存迭代进度;克隆复制当前剩余区间,不恢复初始状态。它不实现 Copy 是标准库 API 选择,可变状态本身不禁止 Copy。

独立示例 03-10(标准库;文件:src/main.rs)。

1
2
3
4
5
6
7
8
9
fn main() {
let mut rest = 5..8;
assert_eq!(rest.next(), Some(5));
assert_eq!(rest, 6..8);
let remaining = rest.clone();
assert_eq!(remaining.collect::<Vec<_>>(), vec![6, 7]);
let countdown: Vec<i32> = (1..=3).rev().collect();
assert_eq!(countdown, vec![3, 2, 1]);
}

Rust 1.96 新增 std::range,将区间描述与迭代进度分开;其中 Range<T> 在 T: Copy 时实现 Copy,可索引切片,并与 ops 版本转换。它不直接实现 Iterator,要用 .iter() 或 .into_iter() 建立独立迭代器。Edition 2024 的 a..b 仍构造 ops::Range,升级编译器不会改变这一点。std::range::Range

自测

  1. mut 能让 [u8; 4] 接收 [u8; 8] 吗?为什么非 Copy 数组要用 from_fn?
  2. &[u8; 4] 与 &[u8] 的长度合同有什么不同?转换会复制哪些数据?
  3. read_id 的短输入、average 的空输入和 first_two 的单字节输入分别返回什么?
  4. get(0..=len) 为什么通常失败?到结尾应怎样写?
  5. 同一字节序列的 &str 与 &[u8],为什么 get(..2) 可能得到不同结果?

参考答案

  1. 不能,长度属于类型。普通重复表达式只求值一次并要求 Copy,from_fn 可以单独构造每个元素。
  2. 前者要求恰好 4 个元素,后者允许运行期不同长度;数组引用转换只借用,转换为拥有数组才复制元素。
  3. 都返回 None,不是默认数值或 panic;zero_out(&mut []) 则正常完成。
  4. 含端点要求终点元素存在,len 不是有效元素下标;尾部写 start..,整段写 ..。
  5. 字符串还检查 UTF-8 字符边界,普通字节切片只检查数值范围。