课程概览 · 第 3 章
上一篇:字符串与切片:String、&str 与 UTF-8 边界
下一篇:表达式与控制流:让分支、循环和返回值清楚可审查
固定 4 字节协议头、运行期增长的任务列表和只读取其中一段的函数,需要不同类型。数组 [T; N] 拥有固定数量的元素,切片引用 &[T] / &mut [T] 借用连续数据,范围描述要选取的区间。本章沿这个顺序设计接口,再处理数值边界与 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
自测 mut 能让 [u8; 4] 接收 [u8; 8] 吗?为什么非 Copy 数组要用 from_fn?&[u8; 4] 与 &[u8] 的长度合同有什么不同?转换会复制哪些数据?read_id 的短输入、average 的空输入和 first_two 的单字节输入分别返回什么?get(0..=len) 为什么通常失败?到结尾应怎样写?同一字节序列的 &str 与 &[u8],为什么 get(..2) 可能得到不同结果? 参考答案 不能,长度属于类型。普通重复表达式只求值一次并要求 Copy,from_fn 可以单独构造每个元素。 前者要求恰好 4 个元素,后者允许运行期不同长度;数组引用转换只借用,转换为拥有数组才复制元素。 都返回 None,不是默认数值或 panic;zero_out(&mut []) 则正常完成。 含端点要求终点元素存在,len 不是有效元素下标;尾部写 start..,整段写 ..。 字符串还检查 UTF-8 字符边界,普通字节切片只检查数值范围。