Rust 系统学习:从基础语法到工程实践 · 结构体、枚举与 trait至用 Result 处理可恢复错误
结构体、枚举与 trait
结构体与方法接收者
结构体把有名字的字段放在一起;impl 提供方法和关联函数,其中 Self 指当前实现的类型。方法的接收者说明它如何使用实例:&self 读取,&mut self 修改,self 获取所有权。没有接收者的 new 是普通关联函数,并不是语言要求的特殊构造器。
#[derive(Debug)]
struct Task {
title: String,
done: bool,
}
impl Task {
fn new(title: String) -> Self {
Self { title, done: false }
}
fn finish(&mut self) {
self.done = true;
}
fn title(&self) -> &str {
&self.title
}
}
fn main() {
let mut task = Task::new(String::from("learn Rust"));
task.finish();
assert!(task.done);
assert_eq!(task.title(), "learn Rust");
println!("{task:?}");
}#[derive(Debug)] 为类型自动实现 Debug 这个 trait,使其支持 {:?} 调试输出;面向用户的 {} 输出由 Display 控制,不能因为派生了 Debug 就使用 {}。后文的 derive(PartialEq) 同样通过自动实现 trait 让值支持相等比较。参见方法语法。
枚举表达状态
枚举可以让每个变体携带不同的数据。Option<T> 的 Some(T) / None 表达有值或无值;用 match 明确处理缺失值,不能把 None 当成任意一种默认数值。
fn first_nonempty(lines: &[&str]) -> Option<usize> {
for index in 0..lines.len() {
if !lines[index].is_empty() {
return Some(index);
}
}
None
}
fn main() {
match first_nonempty(&["", "rust"]) {
Some(index) => assert_eq!(index, 1),
None => panic!("示例应存在非空行"),
}
assert_eq!(first_nonempty(&[]), None);
}0..lines.len() 不包含右端点,保证上例索引在范围内;Some(index) 携带位置,末尾 None 表示遍历后未找到。只关心一个变体时可用 if let Some(value) = ...;需要明确处理每种状态时使用 match。参见枚举与 Option。
泛型与 trait 约束
泛型复用类型结构,trait 描述类型提供的行为。T: Trait 是编译器必须能验证的约束。先写具体类型版本,只有行为确实重复时再抽取泛型。
下例的 struct Note(String); 是元组结构体,拥有一个 String 字段,用 .0 访问。它实现 Summary 后,才能传给要求 T: Summary 的函数。
trait Summary {
fn summary(&self) -> &str;
}
struct Note(String);
impl Summary for Note {
fn summary(&self) -> &str {
&self.0
}
}
fn print_summary<T: Summary>(value: &T) {
println!("{}", value.summary());
}
fn main() {
let note = Note(String::from("ownership first"));
print_summary(¬e);
}impl Trait 也可以简写参数的 trait 约束。需要在运行时保存不同具体类型、并通过同一接口调用时,再学习 dyn Trait trait 对象及其限制。trait 对象不是所有泛型的默认替代品。参见trait及trait 对象。
练习:给 Task 实现 Summary,并用同一个 print_summary 打印 Task 和 Note。
集合、字符串与迭代器
根据需求选容器
| 需求 | 常用类型 | 读取接口 |
|---|---|---|
| 固定个数、同类型元素 | [T; N] | &[T] |
| 可增删的同类型序列 | Vec<T> | &[T] |
| 拥有并可增长的 UTF-8 文本 | String | &str |
| 按键存取数据 | HashMap<K, V> | get 返回 Option<&V> |
Vec 的索引越界会 panic;不确定索引是否有效时用 get。push 可能重新分配底层存储,因此不能在之后还要使用某元素引用时同时修改这个 Vec。这由借用检查保护。参见向量。
fn main() {
let mut scores = vec![60, 70];
scores.push(80);
assert_eq!(scores.get(1), Some(&70));
assert_eq!(scores.get(9), None);
for score in &mut scores {
*score += 1;
}
assert_eq!(scores, [61, 71, 81]);
}字符串不是字符数组
String 和 str 保存 UTF-8;len() 返回字节数。不能用 text[0] 取得所谓第一个字符。chars() 遍历 Unicode 标量值,bytes() 遍历字节。即使 chars() 也不保证一个元素就是一个完整的可见字形。按字节区间截取 &str 必须落在字符边界上;需要安全尝试时用 get。参见字符串。
fn main() {
let text = String::from("你好 Rust");
assert_eq!(text.len(), 11);
assert_eq!(text.chars().count(), 7);
assert_eq!(text.get(..3), Some("你"));
assert_eq!(text.get(..1), None);
}用迭代器表达数据转换
iter() 通常借出元素,iter_mut() 借出可变元素,into_iter() 消耗接收者;具体元素类型由接收者的 IntoIterator 实现决定。map / filter 产生惰性迭代器,需要 collect、sum 或循环等消费者才完成计算。
闭包是可以传给函数的匿名函数,|n| n * 2 中两条竖线之间是参数,后面是返回值表达式,类型通常由上下文推断。下例先用 copied() 将借到的整数复制成整数值,再筛选、转换并收集;filter 的条件闭包借用每个候选值。参见闭包。
fn main() {
let values = vec![1, 2, 3, 4];
let doubled: Vec<i32> = values.iter()
.copied()
.filter(|n| n % 2 == 0)
.map(|n| n * 2)
.collect();
assert_eq!(doubled, [4, 8]);
assert_eq!(values.len(), 4); // 原集合仍然拥有数据
}参见迭代器。回看前面的 first_nonempty,现在可以把循环缩写为 lines.iter().position(|line| !line.is_empty()),保留相同的返回类型和缺失值处理。
需要带键计数时可以使用 HashMap::entry,避免先查询再插入。先理解导入:std 是标准库,:: 分隔路径;use std::collections::HashMap; 让当前作用域可以直接写 HashMap。标准库不需要加入 Cargo.toml 的依赖;自己的模块文件如何组织留到最后一节。参见use 与路径。
use std::collections::HashMap;
fn main() {
let mut counts = HashMap::new();
for word in "rust is rust".split_whitespace() {
let count = counts.entry(word).or_insert(0);
*count += 1;
}
assert_eq!(counts.get("rust"), Some(&2));
}这里的键是借用原字符串的 &str;如果原文本会销毁,而映射仍要保留,通常应让键拥有数据,例如 String。参见HashMap。
用 Result 处理可恢复错误
缺失值用 Option<T>;需要说明失败原因时用 Result<T, E>,成功变体 Ok(T) 携带结果,失败变体 Err(E) 携带错误。它和 Option 一样可以用 match 逐一处理,例如 match "60".parse::<u32>() { Ok(score) => println!("{score}"), Err(error) => println!("{error}") }。parse::<u32>() 显式指定要解析的类型。
函数需要把失败继续交给调用者时再用 ?:在 Ok 时取出值继续执行,在 Err 时提前返回错误,并按需要调用错误转换。它不是忽略错误,也不会自动重试。函数签名要允许这种传播。
use std::num::ParseIntError;
fn parse_scores(text: &str) -> Result<Vec<u32>, ParseIntError> {
text.split_whitespace()
.map(|word| word.parse::<u32>())
.collect()
}
fn average(text: &str) -> Result<Option<f64>, ParseIntError> {
let scores = parse_scores(text)?;
if scores.is_empty() {
return Ok(None);
}
let total: f64 = scores.iter().map(|score| f64::from(*score)).sum();
Ok(Some(total / scores.len() as f64))
}
fn main() {
assert_eq!(average("60 80"), Ok(Some(70.0)));
assert_eq!(average(" "), Ok(None));
assert!(average("60 nope").is_err());
}上例把三种结果分开:有效平均数、没有分数、无法解析。collect 会把一串 Result 收集成整个集合的 Result,遇到错误即停止。这里用浮点数做入门统计,不应直接当成精确财务计算方案。
unwrap / expect 在失败时 panic;仅在失败意味着程序内部假设被破坏等适当场景使用。用户输入、网络与文件读取通常有正常的失败路径,应返回错误或在边界明确处理。参见Result 与传播错误及Result 的迭代收集。
练习:把 main 改成匹配 average 的三类结果并输出不同信息;为超出 u32 范围的输入补一个失败断言。
继续阅读
- 排错
- 排错
- 排错