练习与自测
本章练习共 10 题,答案折叠在每题下方。建议先自己写、编译通过后再展开答案;难度标记:★☆☆ 基础 / ★★☆ 综合 / ★★★ 挑战。
练习 1:结构体更新语法与 move 行为
难度:★☆☆ 要求:定义 struct Server { host: String, port: u16, tls: bool },实现 Server::new、 Server::local(port)。用 ..base 从已有实例派生一个新实例,并证明 base 中 Copy 字段仍可读、 非 Copy 字段已不可读(用注释说明会在哪一行报错)。 提示:注意 String 与 u16 在 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 的自反性、Copy 与 Drop 的冲突。
参考答案(先自己写再看)
参考答案(先自己写再看)
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。Copy与Drop互斥的理由是「一份资源只能被释放一次」:Copy会造出多个独立副本。(d)的FileHandle需要Drop来关闭文件描述符,所以永远不能Copy。
练习 4:手写 Display 与 Debug
难度:★★☆ 要求:实现 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 = None、None。
要点解析:guard(if 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_error,retries 只在 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:Area(fn area(&self) -> f64)、 Summary(fn summary(&self) -> String,默认实现基于 Area)、 Drawable(fn draw(&self) -> Vec<String> 返回若干行字符画,并有默认方法 fn boxed(&self) -> String 把 draw 的每行加上 | 前缀)。为 Circle、Rect、Line 三个类型实现它们。 要求 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: Area 让 Summary 的默认方法里能直接写 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 } },实现 Upper、Reverse、Trim 三个插件, 把它们放进 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));
}提示:Square 有 area 方法,但它满足 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 的固有 area 与 Area::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) -> 个位数 5、Num(42) -> 两位数 42、Word("extraordinarily") -> 长单词(len=15) "extraordinarily"、Sym('(') -> 括号 (。
要点解析:
n @ -9..=9中,范围模式做判断,@把值绑出来,不用写两次。- 分支顺序很重要:
Token::Num(n) if *n < 0必须排在Token::Num(n)前面, 否则负数会被「大数」吃掉(编译器不会替你重排 guard)。 - 因为
t: &Token,Token::Word(w)里的w由默认绑定模式自动成为&String; 在 2024 edition 下不能再写Token::Word(ref w),那会报 「cannot explicitly borrow within an implicitly-borrowing pattern」。 Token::Sym(c @ ('(' | ')'))演示了「@绑定 + 或模式」的组合。