Skip to content

练习与自测

本章练习共 10 题,答案折叠在每题下方。建议先自己写、编译通过后再展开答案;难度标记:★☆☆ 基础 / ★★☆ 综合 / ★★★ 挑战。

练习 1:结构体更新语法与 move 行为

难度:★☆☆ 要求:定义 struct Server { host: String, port: u16, tls: bool },实现 Server::newServer::local(port)。用 ..base 从已有实例派生一个新实例,并证明 baseCopy 字段仍可读、 非 Copy 字段已不可读(用注释说明会在哪一行报错)。 提示:注意 Stringu16 在 move 行为上的区别。

参考答案(先自己写再看)参考答案(先自己写再看)
rust
#[derive(Debug, Clone)]
struct Server {
    host: String,
    port: u16,
    tls: bool,
}

impl Server {
    fn new(host: &str, port: u16, tls: bool) -> Self {
        Self { host: host.to_string(), port, tls } // 字段初始化简写
    }
    fn local(port: u16) -> Self {
        Self::new("127.0.0.1", port, false)
    }
}

fn main() {
    let base = Server::local(8080);
    println!("{base:?}");

    // host 是 String -> 被 move;port/tls 是 Copy -> 被复制
    let tls_server = Server { tls: true, ..base };

    // base.port 仍可读:它是 Copy
    println!("base.port = {}", base.port);
    // base.host 已被 move,下一行会报 E0382: borrow of moved value: `base.host`
    // println!("{}", base.host);

    println!("{tls_server:?}");
    assert_eq!(tls_server.host, "127.0.0.1");
}

要点解析..base 把「没有显式写出的字段」逐个搬走;String 搬走就是 move, u16/bool 搬走是 copy。所以 base 变成「部分移动」状态:能读 port,不能读 host, 也不能整体使用。如果希望 base 保持完整,把 host: base.host.clone() 显式写出来。

练习 3:判断类型能派生哪些 trait

难度:★★☆ 要求:对下面 4 个类型,分别写出「能加哪些 derive」,并说明不能加的那个为什么不能: (a) struct Id(u64); (b) struct Money { cents: i64 } (c) struct Score(f64); (d) struct FileHandle { fd: i32 }(假定它实现了 Drop)。 提示:想想 Eq 的自反性、CopyDrop 的冲突。

参考答案(先自己写再看)参考答案(先自己写再看)
rust
// (a) Id:内部只有 u64,全部 derive 都合法
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
struct Id(u64);

// (b) Money:i64 也全部合法,但业务上要注意 Money 相加不能混币种
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
struct Money { cents: i64 }

// (c) Score:f64 只能 PartialEq / PartialOrd,不能 Eq / Ord / Hash
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Default)]
struct Score(f64);
// #[derive(Eq)]  -> 错误:the trait bound `f64: Eq` is not satisfied
// 用作 HashMap 键时会报 `f64: Hash` 未满足

// (d) FileHandle:实现了 Drop,因此不能 Copy
struct FileHandle { fd: i32 }
impl Drop for FileHandle {
    fn drop(&mut self) { println!("close fd {}", self.fd); }
}
// #[derive(Copy, Clone)] -> 错误 E0184: the trait `Copy` cannot be implemented
// for a type that also implements `Drop`
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct SafeHandle { fd: i32 } // 这个可以,只要不加 Drop

fn main() {
    println!("{:?} {:?} {:?}", Id::default(), Money::default(), Score::default());
}

要点解析

  • Eq 要求「自反、传递、对称」,NaN != NaN 破坏了自反性,所以 f64 只能是 PartialEq
  • Hash 要求 k1 == k2 => hash(k1) == hash(k2)。浮点不满足 Eq,所以也不能 Hash
  • CopyDrop 互斥的理由是「一份资源只能被释放一次」:Copy 会造出多个独立副本。
  • (d)FileHandle 需要 Drop 来关闭文件描述符,所以永远不能 Copy

练习 4:手写 DisplayDebug

难度:★★☆ 要求:实现 struct Fraction { num: i64, den: i64 },让 {} 输出 3/4; 当分母为 1 时只输出整数 3;分母为 0 时输出 3/0 (invalid)。 再实现 Debug 输出 Fraction { num: 3, den: 4 } 的形式(不用 derive)。 提示write! 返回 fmt::Result,多个 write! 之间要 ?

参考答案(先自己写再看)参考答案(先自己写再看)
rust
use std::fmt;

struct Fraction { num: i64, den: i64 }

impl Fraction {
    fn new(num: i64, den: i64) -> Self { Self { num, den } }
}

impl fmt::Display for Fraction {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        if self.den == 0 {
            return write!(f, "{}/0 (invalid)", self.num);
        }
        if self.den == 1 {
            return write!(f, "{}", self.num);
        }
        write!(f, "{}/{}", self.num, self.den)
    }
}

impl fmt::Debug for Fraction {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        // debug_struct 让输出格式与 derive(Debug) 完全一致
        f.debug_struct("Fraction")
            .field("num", &self.num)
            .field("den", &self.den)
            .finish()
    }
}

fn main() {
    println!("{}", Fraction::new(3, 4)); // 3/4
    println!("{}", Fraction::new(3, 1)); // 3
    println!("{}", Fraction::new(3, 0)); // 3/0 (invalid)
    println!("{:?}", Fraction::new(3, 4)); // Fraction { num: 3, den: 4 }
    println!("{:#?}", Fraction::new(3, 4)); // 多行版
}

要点解析write! 返回 fmt::Result,所以可以直接 return write!(...) 或让它做尾表达式。 f.debug_struct(..) 是标准库提供的「Debug 构建器」,用它可以精确控制字段的显示与顺序; 若想跳过某个字段,链式调用里不写 .field(..) 即可。

练习 5:用 Option 表达可能无解的计算

难度:★★☆ 要求:实现本章的 enum Shape { Circle(f64), Rect { w: f64, h: f64 }, Triangle(f64, f64, f64) }, 加一个 perimeter(&self) -> Option<f64>:三角形两边之和必须大于第三边,否则返回 None。 用 filter/match 任选实现,并在 main 里跑一个非法三角形。 提示Option 比返回 -1.0 更能表达「无法计算」。

参考答案(先自己写再看)参考答案(先自己写再看)
rust
#[derive(Debug, Clone, Copy, PartialEq)]
enum Shape {
    Circle(f64),
    Rect { w: f64, h: f64 },
    Triangle(f64, f64, f64),
}

impl Shape {
    fn area(&self) -> f64 {
        match *self {
            Shape::Circle(r) => std::f64::consts::PI * r * r,
            Shape::Rect { w, h } => w * h,
            Shape::Triangle(a, b, c) => {
                let s = (a + b + c) / 2.0;
                (s * (s - a) * (s - b) * (s - c)).max(0.0).sqrt()
            }
        }
    }

    // 无法计算时返回 None,而不是 -1.0 这种“魔法值”
    fn perimeter(&self) -> Option<f64> {
        match *self {
            Shape::Circle(r) if r > 0.0 => Some(2.0 * std::f64::consts::PI * r),
            Shape::Circle(_) => None,
            Shape::Rect { w, h } if w > 0.0 && h > 0.0 => Some(2.0 * (w + h)),
            Shape::Rect { .. } => None,
            Shape::Triangle(a, b, c) => {
                let ok = a > 0.0 && b > 0.0 && c > 0.0
                    && a + b > c && a + c > b && b + c > a; // 三角不等式
                ok.then(|| a + b + c) // bool::then:true 时求值闭包
            }
        }
    }
}

fn main() {
    let valid = [
        Shape::Circle(1.0),
        Shape::Rect { w: 3.0, h: 4.0 },
        Shape::Triangle(3.0, 4.0, 5.0),
    ];
    for s in &valid { println!("{:?} area={:.4} perim={:?}", s, s.area(), s.perimeter()); }

    // 非法:1 + 2 < 10,不能构成三角形
    let bad = Shape::Triangle(1.0, 2.0, 10.0);
    println!("bad perim = {:?}", bad.perimeter()); // None
    // 边长非正
    println!("{:?}", Shape::Rect { w: -1.0, h: 2.0 }.perimeter()); // None
}

输出(节选):bad perim = NoneNone

要点解析guardif r > 0.0)让非法输入落到下一个分支。 bool::then(Rust 1.50+)把「条件成立才求值」写得比 if ok { Some(x) } else { None } 更短; then_some 则是「立即求值」。若想避免给非法三角形也打印一个伪造的面积, area 也应该返回 Option<f64> —— 这是「让非法状态不可表示」的延伸。

练习 6:把字符串状态机重构为枚举

难度:★★★ 要求:把下面这段「用字符串做状态机」的代码改成枚举版本,要求非法转换在编译期不可能发生、 失败时返回 Result<(), String>

rust
struct Job { status: String, retries: u32, last_error: Option<String> }
// status 只会是 "pending" / "running" / "done" / "failed",靠字符串比较判断

提示Failed 变体自然携带 last_errorretries 只在 Running 时才有意义。

参考答案(先自己写再看)参考答案(先自己写再看)
rust
#[derive(Debug, Clone, PartialEq)]
enum Status {
    Pending,
    Running { attempt: u32 },
    Done,
    Failed { error: String },
}

#[derive(Debug)]
struct Job { name: String, status: Status }

impl Job {
    fn new(name: &str) -> Self {
        Self { name: name.to_string(), status: Status::Pending }
    }

    fn start(&mut self) -> Result<(), String> {
        match &self.status {
            Status::Pending => { self.status = Status::Running { attempt: 1 }; Ok(()) }
            Status::Failed { .. } => { self.status = Status::Running { attempt: 1 }; Ok(()) }
            Status::Running { .. } => Err("任务已在运行".to_string()),
            Status::Done => Err("任务已完成".to_string()),
        }
    }

    fn succeed(&mut self) -> Result<(), String> {
        match self.status {
            Status::Running { .. } => { self.status = Status::Done; Ok(()) }
            _ => Err("只有运行中的任务能成功结束".to_string()),
        }
    }

    fn fail(&mut self, msg: &str) -> Result<(), String> {
        match self.status {
            Status::Running { attempt } => {
                self.status = Status::Failed { error: msg.to_string() };
                println!("第 {attempt} 次尝试失败");
                Ok(())
            }
            _ => Err("只有运行中的任务能失败".to_string()),
        }
    }

    // 编译期保证:只有 Done 才算成功
    fn is_success(&self) -> bool { matches!(self.status, Status::Done) }
}

fn main() {
    let mut j = Job::new("import");
    println!("{:?}", j.start());                       // Ok(())
    println!("{:?}", j.start());                       // Err("任务已在运行")
    println!("{:?}", j.fail("网络超时"));               // 第 1 次尝试失败
    println!("{:?}", j.succeed());                     // Err(...)
    println!("{:?}", j.start());                       // 失败后可重试 -> Ok(())
    println!("{:?}", j.succeed());                     // Ok(())
    println!("success = {}", j.is_success());
    println!("{j:?}");
}

要点解析Failed 变体自己携带 error 字符串,因此不存在「状态是 failed 但错误信息是 None」这种矛盾状态;attempt 只在 Running 里存在,不会在 Pending 时被误读。 Status 没有实现 Copy(含 String),所以 succeed/fail 里用 match self.status 会 尝试 move 出字段 —— 这里能编译是因为每个分支都立即重新赋值,编译器允许这种 「先取出再写回」的模式;若分支不再赋值,就要写 match &self.status 并克隆。

练习 8:supertrait 与默认方法建模

难度:★★★ 要求:定义三个 trait:Areafn area(&self) -> f64)、 Summaryfn summary(&self) -> String,默认实现基于 Area)、 Drawablefn draw(&self) -> Vec<String> 返回若干行字符画,并有默认方法 fn boxed(&self) -> Stringdraw 的每行加上 | 前缀)。为 CircleRectLine 三个类型实现它们。 要求 Summary 是 supertrait 关系(trait Summary: Area)。 提示:默认方法里可以通过 self.area() 调用 supertrait 的方法。

参考答案(先自己写再看)参考答案(先自己写再看)
rust
trait Area {
    fn area(&self) -> f64;
}

// supertrait:实现 Summary 的类型必须先实现 Area,默认方法里可以调用 self.area()
trait Summary: Area {
    fn label(&self) -> String;
    fn summary(&self) -> String {
        format!("{} 面积={:.3}", self.label(), self.area())
    }
}

trait Drawable {
    fn draw(&self) -> Vec<String>;
    fn boxed(&self) -> String {
        self.draw()
            .iter()
            .map(|line| format!("| {line}"))
            .collect::<Vec<_>>()
            .join("\n")
    }
}

#[derive(Debug, Clone, Copy)]
struct Circle { r: f64 }
#[derive(Debug, Clone, Copy)]
struct Rect { w: f64, h: f64 }
#[derive(Debug, Clone, Copy)]
struct Line { len: f64 }

impl Area for Circle { fn area(&self) -> f64 { std::f64::consts::PI * self.r * self.r } }
impl Area for Rect { fn area(&self) -> f64 { self.w * self.h } }
impl Area for Line { fn area(&self) -> f64 { 0.0 } } // 线没有面积

impl Summary for Circle { fn label(&self) -> String { format!("圆 r={}", self.r) } }
impl Summary for Rect { fn label(&self) -> String { format!("矩形 {}x{}", self.w, self.h) } }
impl Summary for Line {
    fn label(&self) -> String { format!("线段 len={}", self.len) }
    fn summary(&self) -> String { format!("{} 面积=无", self.label()) } // 覆盖默认实现
}

impl Drawable for Circle {
    fn draw(&self) -> Vec<String> {
        vec!["  ***  ".to_string(), " ***** ".to_string(), "  ***  ".to_string()]
    }
}
impl Drawable for Rect {
    fn draw(&self) -> Vec<String> {
        vec!["+-----+".to_string(), "|     |".to_string(), "+-----+".to_string()]
    }
}
impl Drawable for Line {
    fn draw(&self) -> Vec<String> { vec!["--------".to_string()] }
}

fn main() {
    fn report<T: Summary + Drawable>(item: &T) {
        println!("{}", item.summary());
        println!("{}", item.boxed());
    }
    report(&Circle { r: 2.0 });
    report(&Rect { w: 3.0, h: 4.0 });
    report(&Line { len: 8.0 });
}

要点解析trait Summary: AreaSummary 的默认方法里能直接写 self.area() —— supertrait 就是「实现我的前提条件」。Line 覆盖了 summary 默认实现,因为「面积 0」 在语义上是错的(线根本没有面积),这说明默认方法是可被覆盖的约定而非硬性规则。 report 用了 T: Summary + Drawable,即多个 trait bound 用 + 叠加。

练习 9:trait 对象实现异构集合排序

难度:★★★ 要求:定义 trait Plugin { fn name(&self) -> &str; fn run(&self, input: &str) -> String; fn priority(&self) -> u8 { 0 } },实现 UpperReverseTrim 三个插件, 把它们放进 Vec<Box<dyn Plugin>>,按 priority() 从高到低排序后依次 run 同一段输入。 提示sort_by_key 需要 Reverse 包装或 sort_by(|a, b| b.priority().cmp(&a.priority()))

参考答案(先自己写再看)参考答案(先自己写再看)
rust
trait Plugin {
    fn name(&self) -> &str;
    fn priority(&self) -> u8 { 0 } // 默认实现:普通插件优先级 0
    fn run(&self, input: &str) -> String;
}

struct Upper;
impl Plugin for Upper {
    fn name(&self) -> &str { "upper" }
    fn priority(&self) -> u8 { 10 }
    fn run(&self, input: &str) -> String { input.to_uppercase() }
}

struct Reverse;
impl Plugin for Reverse {
    fn name(&self) -> &str { "reverse" }
    fn priority(&self) -> u8 { 5 }
    fn run(&self, input: &str) -> String { input.chars().rev().collect() }
}

struct Trim;
impl Plugin for Trim {
    fn name(&self) -> &str { "trim" } // priority 用默认值 0
    fn run(&self, input: &str) -> String { input.trim().to_string() }
}

fn main() {
    // 异构集合:元素具体类型不同,统一擦除成 Box<dyn Plugin>
    let mut plugins: Vec<Box<dyn Plugin>> = vec![
        Box::new(Upper),
        Box::new(Reverse),
        Box::new(Trim),
    ];

    // 高优先级在前
    plugins.sort_by(|a, b| b.priority().cmp(&a.priority()));

    let mut data = String::from("  hello rust  ");
    for p in &plugins {
        data = p.run(&data);
        println!("[{}] p={} -> {:?}", p.name(), p.priority(), data);
    }
    println!("final = {data:?}");
}

输出:

text
[upper] p=10 -> "  HELLO RUST  "
[reverse] p=5 -> "  TSUR OLLEH  "
[trim] p=0 -> "TSUR OLLEH"
final = "TSUR OLLEH"

要点解析sort_by(|a, b| b.priority().cmp(&a.priority())) 用参数顺序表达「降序」, 比 sort_by_key(|p| Reverse(p.priority())) 更直观。 Box<dyn Plugin> 里存的是「数据指针 + vtable 指针」的胖指针(16 字节), 调用 p.run(..) 时通过 vtable 间接跳转。三个插件类型都在同一个 Vec 里, 这在泛型 Vec<T> 中是不可能的。

练习 10:关联类型与泛型参数的取舍

难度:★★★ 要求:分别用「关联类型」和「泛型参数」两种方式定义 trait Decode { ... }decode(&self, raw: &str) -> ...。 在同一个类型 Csv 上同时实现「解析为 Vec<String>」和「解析为 HashMap<String,String>」。 指出哪种写法能做到,为什么。 提示:一个类型只能有一个关联类型取值,但可以有多个泛型实现。

参考答案(先自己写再看)参考答案(先自己写再看)
rust
use std::collections::HashMap;

// ── 泛型参数版:一个类型可以有多个实现 ──
trait DecodeGeneric<T> {
    fn decode(&self, raw: &str) -> T;
}

struct Csv;

impl DecodeGeneric<Vec<String>> for Csv {
    fn decode(&self, raw: &str) -> Vec<String> {
        raw.split(',').map(|s| s.trim().to_string()).collect()
    }
}

impl DecodeGeneric<HashMap<String, String>> for Csv {
    fn decode(&self, raw: &str) -> HashMap<String, String> {
        raw.split(',')
            .filter_map(|pair| pair.split_once('='))
            .map(|(k, v)| (k.trim().to_string(), v.trim().to_string()))
            .collect()
    }
}

// ── 关联类型版:一个类型只能有一个 Output ──
trait Decode {
    type Output;
    fn decode(&self, raw: &str) -> Self::Output;
}

impl Decode for Csv {
    type Output = Vec<String>;              // 只能选一个!
    fn decode(&self, raw: &str) -> Vec<String> {
        raw.split(',').map(|s| s.trim().to_string()).collect()
    }
}

// 关联类型版的好处:调用处不必写类型参数,返回类型自动推导
fn decode_all<D: Decode>(d: &D, raw: &str) -> D::Output { d.decode(raw) }

fn main() {
    let csv = Csv;

    // 泛型版:必须显式指定要哪一个实现(turbofish 或类型标注)
    let list: Vec<String> = DecodeGeneric::decode(&csv, "a, b, c");
    let map: HashMap<String, String> =
        DecodeGeneric::<HashMap<String, String>>::decode(&csv, "a=1, b=2");
    println!("{list:?}");
    println!("{map:?}");

    // 关联类型版:无需标注
    println!("{:?}", decode_all(&csv, "x,y"));
}

结论只有泛型参数版能做到「同一类型多种解码目标」,因为关联类型 Output 在一个 impl 里只能有一个取值;再写第二个 impl Decode for Csv 会报 E0119 conflicting implementations。 反过来说,如果你的语义是「一个类型天然只有一种答案」,就该用关联类型: 调用处不用标注类型,fn decode_all<D: Decode> 的签名也更干净(Iterator 就是这么设计的)。

练习 11:判断 trait 代码能否编译

难度:★★★ 要求:不改代码,判断下面这段是否能编译;能编译就写出输出,不能就指出错误码与修法。

rust
trait Area { fn area(&self) -> f64; }

struct Circle { r: f64 }
impl Area for Circle {
    fn area(&self) -> f64 { std::f64::consts::PI * self.r * self.r }
}

struct Square { side: f64 }
impl Square { fn area(&self) -> f64 { self.side * self.side } }

fn total(shapes: &[Box<dyn Area>]) -> f64 { shapes.iter().map(|s| s.area()).sum() }

fn main() {
    let shapes: Vec<Box<dyn Area>> = vec![
        Box::new(Circle { r: 1.0 }),
        Box::new(Square { side: 2.0 }),
    ];
    println!("{:.3}", total(&shapes));
}

提示Squarearea 方法,但它满足 dyn Area 吗?

参考答案(先自己写再看)参考答案(先自己写再看)

不能编译。 错误码 E0277

text
error[E0277]: the trait bound `Square: Area` is not satisfied
 --> src/main.rs:9:72
  |
9 |     let shapes: Vec<Box<dyn Area>> = vec![Box::new(Circle { r: 1.0 }),
  |                                              Box::new(Square { side: 2.0 })];
  |                                              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  |                                              unsatisfied trait bound
help: the trait `Area` is not implemented for `Square`
help: the trait `Area` is implemented for `Circle`
  = note: required for the cast from `Box<Square>` to `Box<dyn Area>`

原因Square 有一个固有方法 area,名字虽然一样,但它没有 impl Area for Square。 Rust 是名义化类型系统:方法签名相同不代表实现了 trait,必须显式写 impl。 把 Square 放进 Vec<Box<dyn Area>> 需要把它强制转换成 dyn Area,而 Square: Area 不成立。

修法(二选一):

rust
// 修法 1:补上 trait 实现(推荐,让 Square 也成为一种 Shape)
trait Area { fn area(&self) -> f64; }
struct Square { side: f64 }
impl Area for Square {
    fn area(&self) -> f64 { self.side * self.side }
}
fn main() { println!("{:.3}", Area::area(&Square { side: 2.0 })); }
rust
// 修法 2:不要 dyn,用一个能容纳两种类型的枚举
trait Area { fn area(&self) -> f64; }
struct Circle { r: f64 }
struct Square { side: f64 }
impl Area for Circle { fn area(&self) -> f64 { std::f64::consts::PI * self.r * self.r } }
impl Area for Square { fn area(&self) -> f64 { self.side * self.side } }

enum Any { Circle(Circle), Square(Square) }
impl Area for Any {
    fn area(&self) -> f64 {
        match self { Any::Circle(c) => c.area(), Any::Square(s) => s.area() }
    }
}

fn main() {
    let shapes = [Any::Circle(Circle { r: 1.0 }), Any::Square(Square { side: 2.0 })];
    let total: f64 = shapes.iter().map(|s| s.area()).sum();
    println!("{total:.3}"); // 7.142
}

补上 impl Area for Square 后,程序输出 7.142 (π·1² + 2² = 3.14159 + 4 ≈ 7.142)。

要点解析:这类题培养的是「同名方法 ≠ 实现 trait」的直觉。 另一条相关直觉:Square 的固有 areaArea::area 若同时存在, Square::area(&sq) 会优先解析到固有方法 —— 这正是「多个 impl 块与方法/字段同名」提到的固有方法优先级。

练习 12:枚举的综合模式匹配

难度:★★☆ 要求:定义 enum Token { Num(i64), Word(String), Sym(char), Eof }, 写 fn describe(t: &Token) -> String 使用:范围模式、|@ 绑定、哨兵(guard)、 嵌套解构(Token::Word(ref w)w.len() 判断)、_ 兜底。 提示Num(n @ -9..=9) 这类组合能一次完成「判断 + 绑定」。

参考答案(先自己写再看)参考答案(先自己写再看)
rust
#[derive(Debug, Clone)]
enum Token {
    Num(i64),
    Word(String),
    Sym(char),
    Eof,
}

fn describe(t: &Token) -> String {
    match t {
        // 范围模式 + @ 绑定:一次完成判断与绑定
        Token::Num(n @ -9..=9) => format!("个位数 {n}"),
        Token::Num(n @ 10..=99) => format!("两位数 {n}"),
        Token::Num(n) if *n < 0 => format!("负数 {n}"),
        Token::Num(n) => format!("大数 {n}"),
        // 嵌套解构 + 借用:t 是 &Token,默认绑定模式让 w 自动成为 &String
        Token::Word(w) if w.len() > 10 => format!("长单词(len={}) {:?}", w.len(), w),
        Token::Word(w) if w.is_empty() => "空单词".to_string(),
        Token::Word(w) => format!("单词 {w:?}"),
        // | 或模式:多个值共用同一分支
        Token::Sym('+') | Token::Sym('-') | Token::Sym('*') | Token::Sym('/') => {
            format!("运算符 {}", t_as_char(t))
        }
        Token::Sym(c @ ('(' | ')' | '[' | ']' | '{' | '}')) => format!("括号 {c}"),
        Token::Sym(c) => format!("符号 {c:?}"),
        Token::Eof => "结束".to_string(),
    }
}

// 小工具:从 &Token 里安全取出 Sym 的字符
fn t_as_char(t: &Token) -> char {
    match t { Token::Sym(c) => *c, _ => '?' }
}

fn main() {
    let tokens = vec![
        Token::Num(5),
        Token::Num(42),
        Token::Num(-7),
        Token::Num(1000),
        Token::Word("hi".into()),
        Token::Word("extraordinarily".into()),
        Token::Word(String::new()),
        Token::Sym('+'),
        Token::Sym('('),
        Token::Sym('#'),
        Token::Eof,
    ];
    for t in &tokens { println!("{:?} -> {}", t, describe(t)); }
}

输出(节选):Num(5) -> 个位数 5Num(42) -> 两位数 42Word("extraordinarily") -> 长单词(len=15) "extraordinarily"Sym('(') -> 括号 (

要点解析

  • n @ -9..=9 中,范围模式做判断,@ 把值绑出来,不用写两次
  • 分支顺序很重要:Token::Num(n) if *n < 0 必须排在 Token::Num(n) 前面, 否则负数会被「大数」吃掉(编译器不会替你重排 guard)。
  • 因为 t: &TokenToken::Word(w) 里的 w 由默认绑定模式自动成为 &String; 在 2024 edition 下不能再Token::Word(ref w),那会报 「cannot explicitly borrow within an implicitly-borrowing pattern」。
  • Token::Sym(c @ ('(' | ')')) 演示了「@ 绑定 + 或模式」的组合。

本章小结 / 自测清单

内容以 rustc 1.98.1 · Rust 2024 edition 为基准