Skip to content

Rust 系统学习:从基础语法到工程实践 · 结构体、枚举与 trait至用 Result 处理可恢复错误 ​

返回分篇目录

结构体、枚举与 trait ​

结构体与方法接收者 ​

结构体把有名字的字段放在一起;impl 提供方法和关联函数,其中 Self 指当前实现的类型。方法的接收者说明它如何使用实例:&self 读取,&mut self 修改,self 获取所有权。没有接收者的 new 是普通关联函数,并不是语言要求的特殊构造器。

rust
#[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 当成任意一种默认数值。

rust
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 的函数。

rust
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(&note);
}

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。这由借用检查保护。参见向量。

rust
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。参见字符串。

rust
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 的条件闭包借用每个候选值。参见闭包。

rust
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 与路径。

rust
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 时提前返回错误,并按需要调用错误转换。它不是忽略错误,也不会自动重试。函数签名要允许这种传播。

rust
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 范围的输入补一个失败断言。

继续阅读