Skip to content

练习与自测

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

代码按 rustc --edition 2024 编译,可直接放进单个文件。

练习 1:泛型栈与 FromIterator

难度:★★☆ 要求:实现泛型 Stack<T>,支持 newpushpoppeeklenis_empty。 再为 T: Display 额外实现一个 print_all 方法(放在单独的 impl 块里), 并让 Stack<T> 实现 FromIterator<T>,使 Vec<T>::into_iter().collect::<Stack<T>>() 可用。 提示:内部用 Vec<T>pop 返回 Option<T>peek 返回 Option<&T>

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

#[derive(Debug)]
struct Stack<T> {
    items: Vec<T>,
}

impl<T> Stack<T> {
    fn new() -> Self {
        Stack { items: Vec::new() }
    }

    fn push(&mut self, value: T) {
        self.items.push(value);
    }

    fn pop(&mut self) -> Option<T> {
        self.items.pop()
    }

    fn peek(&self) -> Option<&T> {
        self.items.last()
    }

    fn len(&self) -> usize {
        self.items.len()
    }

    fn is_empty(&self) -> bool {
        self.items.is_empty()
    }
}

// 只有 T: Display 的栈才有 print_all:约束写在 impl 块上,而不是结构体上
impl<T: Display> Stack<T> {
    fn print_all(&self) {
        for item in &self.items {
            println!("{item}");
        }
    }
}

impl<T> FromIterator<T> for Stack<T> {
    fn from_iter<I: IntoIterator<Item = T>>(iter: I) -> Self {
        Stack { items: iter.into_iter().collect() }
    }
}

fn main() {
    let mut stack: Stack<i32> = Stack::new();
    stack.push(1);
    stack.push(2);
    stack.push(3);
    println!("{:?} {}", stack.peek(), stack.len());
    println!("{:?}", stack.pop());
    println!("{}", stack.is_empty());
    stack.print_all();

    // FromIterator 让 collect 能直接构造 Stack
    let collected: Stack<char> = "abc".chars().collect();
    println!("{:?}", collected);
}

输出:

Some(3) 3
Some(3)
false
1
2
Stack { items: ['a', 'b', 'c'] }

要点解析

  • 约束放在 impl 块上(impl<T: Display> Stack<T>)而不是结构体上,这样 Stack<NoDisplay> 依然可用。
  • peek 返回 Option<&T> 而不是 Option<T>,避免要求 T: CopyClone
  • FromIterator<T> 的泛型参数是 T 而不是 Selfcollect() 的目标类型由标注决定。
  • 常见坑:同一表达式里同时 stack.peek()stack.pop() 会触发 E0502(不可变 + 可变借用冲突), 所以要拆成两条语句。

练习 2:用 where 子句整理复杂约束

难度:★★☆ 要求:下面这个函数把多个约束都挤在 <> 里,请用 where 子句重写,保持行为一致; 并额外加上一条「T::Id 必须能 Debug」的约束。

rust
trait Named {
    type Id;
    fn id(&self) -> Self::Id;
    fn name(&self) -> &str;
}

// 待改写:把这些约束搬到 where 子句里
fn render<T: Named + Clone, U: std::fmt::Display>(item: T, label: U) -> String {
    let copy = item.clone();
    format!("{} -> {label}", copy.name())
}

提示where 里每行一个约束;关联类型约束只能写在 where 里。

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

trait Named {
    type Id;
    fn id(&self) -> Self::Id;
    fn name(&self) -> &str;
}

#[derive(Clone)]
struct User {
    id: u64,
}

impl Named for User {
    type Id = u64;
    fn id(&self) -> Self::Id {
        self.id
    }
    fn name(&self) -> &str {
        "user"
    }
}

// 原签名:fn render<T: Named + Clone, U: Display>(item: T, label: U) -> String
fn render<T, U>(item: T, label: U) -> String
where
    T: Named + Clone,
    U: Display,
    T::Id: Debug,
{
    let copy = item.clone();
    format!("{:?} {} {label}", copy.id(), item.name())
}

fn main() {
    println!("{}", render(User { id: 7 }, "label"));
}

输出:7 user label

要点解析

  • where 里可以写内联语法写不出的约束:T::Id: Debug(关联类型约束)就是这样一条。 虽然较新的 Rust 支持 T: Named<Id: Debug> 这种「关联类型约束简写」, 但 where T::Id: Debug 在任何支持泛型的版本上都可读且通用。
  • item.clone() 的返回值需要 T: Clonecopy.id() 需要 T::Id: Debugitem.name() 需要 T: Named——三处约束缺一不可,报错会分别指向它们。
  • U: Display 只被 format! 使用。

练习 3:为 Vec<T> 批量实现 trait

难度:★★☆ 要求:定义 trait Summary { fn summarize(&self) -> String; },为 News 实现它, 再写 impl<T: Summary> Summary for Vec<T>,把每个元素的摘要用 " | " 连接。 提示Vec<T> 的实现里先 mapjoin

参考答案(先自己写再看)参考答案(先自己写再看)
rust
trait Summary {
    fn summarize(&self) -> String;
}

struct News {
    headline: String,
    views: u32,
}

impl Summary for News {
    fn summarize(&self) -> String {
        format!("{} ({} views)", self.headline, self.views)
    }
}

struct Tweet {
    user: String,
}

impl Summary for Tweet {
    fn summarize(&self) -> String {
        format!("@{}", self.user)
    }
}

// 递归式实现:Vec<T> 的摘要由元素的摘要拼成
impl<T: Summary> Summary for Vec<T> {
    fn summarize(&self) -> String {
        self.iter()
            .map(|item| item.summarize())
            .collect::<Vec<_>>()
            .join(" | ")
    }
}

fn main() {
    // Vec 的元素必须同类型:两条 News 合成一个 feed
    let feed = vec![
        News { headline: "Rust 1.98".into(), views: 120 },
        News { headline: "Rust 2024".into(), views: 80 },
    ];
    println!("{}", feed.summarize());
    // Tweet 单独用,展示同一个 trait 的不同实现
    println!("{}", Tweet { user: "ferris".into() }.summarize());
    println!("{}", Vec::<News>::new().summarize());
}

输出:

Rust 1.98 (120 views) | Rust 2024 (80 views)
@ferris

要点解析

  • Vecsummarize 返回空字符串,join 对空集合就是这个语义,不需要特判。
  • 这种「容器转发到元素」的实现完全符合孤儿规则吗?符合——Summary 是本 crate 定义的 trait, 所以可以为外部类型 Vec<T> 实现(条件里不涉及外部类型参数即可)。
  • 注意递归实现的边界:如果将来加 impl Summary for StringVec<String> 仍然只能有一种实现, 不会冲突;但如果再加一个无约束的 impl Summary for Vec<T>,就会得到 E0119。

练习 5:带生命周期的迭代器结构

难度:★★★ 要求:用生命周期实现 struct Splitter<'a> { rest: &'a str }next_line(&mut self) -> Option<&'a str>:按 \n 切分,返回的每一行都不能借用 &mut self,而是直接指向原输入; 输入末尾没有换行时剩余部分也算一行,全部取完后返回 None提示find/split_at;先把 self.rest 复制出来(&strCopy)避免借用冲突。

参考答案(先自己写再看)参考答案(先自己写再看)
rust
struct Splitter<'a> {
    rest: &'a str,
}

impl<'a> Splitter<'a> {
    fn new(input: &'a str) -> Self {
        Splitter { rest: input }
    }

    // 返回 'a:行直接指向原输入,与 &mut self 的借用无关
    fn next_line(&mut self) -> Option<&'a str> {
        if self.rest.is_empty() {
            return None;
        }
        match self.rest.find('\n') {
            Some(index) => {
                let (line, tail) = self.rest.split_at(index);
                // tail 以 '\n' 开头,跳过它;因为 rest 是 &'a str,切片后仍是 &'a str
                self.rest = &tail[1..];
                Some(line)
            }
            None => {
                let line = self.rest;
                self.rest = "";
                Some(line)
            }
        }
    }
}

fn main() {
    let text = String::from("alpha\nbeta\n\ngamma");
    let mut splitter = Splitter::new(&text);
    while let Some(line) = splitter.next_line() {
        println!("[{line}]");
    }

    // 返回的行不借用 splitter,因此 splitter 可以立刻消亡
    let line = {
        let mut s = Splitter::new("one\ntwo");
        s.next_line()
    };
    println!("still valid: {line:?}");
}

输出:

[alpha]
[beta]
[]
[gamma]
still valid: Some("one")

要点解析

  • -> Option<&'a str>-> Option<&str> 的区别是本练习的核心。后者按省略规则 3 绑定到 &mut self, 于是返回的行会一直「锁住」splitter(不可再调用 next_line),也无法在 splitter 消亡后使用。
  • &tail[1..] 之所以还是 &'a str,是因为切片操作在 &'a str 上进行,派生引用继承 'a; 这里不需要临时值,因此不会触发 E0716。
  • 若写成 self.rest = &self.rest[index + 1..] 也能编译(在 &'a str 上取切片), 但把 rest 先取出来(let rest = self.rest;)在更复杂的逻辑里能避免借用冲突。

练习 6:返回 impl Trait 的类型一致性

难度:★★☆ 要求:下面函数为什么不编译?给出两种修法并说明各自的代价。

rust
// ❌ 无法编译:两个分支返回不同的具体类型,而 RPIT 只能有一个隐藏类型
fn numbers(flag: bool) -> impl Iterator<Item = i32> {
    if flag { vec![1, 2].into_iter() } else { 3..5 }
}

提示:关注 RPIT 的「只有一个隐藏类型」。

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

不通过编译。RPIT 的不透明类型只有一个隐藏类型;if/else 两个分支分别返回 std::vec::IntoIter<i32>std::ops::Range<i32>,编译器无法为 impl Iterator 选出唯一类型:

error[E0308]: `if` and `else` have incompatible types
 --> src/main.rs:2:47
  |
2 |     if flag { vec![1, 2].into_iter() } else { 3..5 }
  |               ----------------------          ^^^^ expected `IntoIter<{integer}>`,
  |                                                    found `Range<{integer}>`
  |
  = note: expected struct `std::vec::IntoIter<{integer}>`
             found struct `std::ops::Range<{integer}>`
help: you could change the return type to be a boxed trait object
  |
1 - fn numbers(flag: bool) -> impl Iterator<Item = i32> {
1 + fn numbers(flag: bool) -> Box<dyn Iterator<Item = i32>> {

修法一:装箱成 trait object(代价:一次堆分配 + 虚调用)

rust
fn numbers(flag: bool) -> Box<dyn Iterator<Item = i32>> {
    if flag {
        Box::new(vec![1, 2].into_iter())
    } else {
        Box::new(3..5)
    }
}

fn main() {
    println!("{:?}", numbers(true).collect::<Vec<_>>());
    println!("{:?}", numbers(false).collect::<Vec<_>>());
}

修法二:统一成同一个类型(代价:可能需要一次 collect,但保持静态派发)

rust
fn numbers(flag: bool) -> impl Iterator<Item = i32> {
    // 两个分支都产出 Vec<i32>,类型一致
    let values = if flag { vec![1, 2] } else { (3..5).collect() };
    values.into_iter()
}

fn main() {
    println!("{:?}", numbers(true).collect::<Vec<_>>());
    println!("{:?}", numbers(false).collect::<Vec<_>>());
}

输出(两种修法相同):

[1, 2]
[3, 4]

要点解析impl Trait静态派发,返回类型在编译期必须是唯一的; 需要「返回多种类型」时必须换成 dyn Trait。反之,如果在意性能,就用修法二: 一次 collect 的代价通常远小于每个元素一次虚调用。

练习 7:生命周期变型的编译判断

难度:★★★ 要求:判断下列 4 行代码哪些能编译,并解释原因:

rust
// ❌ 整段无法编译:A 行要求 Cell<&'static str> 当作 Cell<&'a str>,而 Cell<T> 对 T 不变
use std::cell::Cell;
fn set<'a>(cell: &Cell<&'a str>, value: &'a str) { cell.set(value); }
fn main() {
    let cell: Cell<&'static str> = Cell::new("static");
    let s = String::from("temporary");
    set(&cell, &s);            // A
    let r: &str = "static";    // B
    let short: &str = r;       // C
    let mut m: &'static str = "x";
    m = short;                 // D
}

提示Cell<T>T 不变;&'a T'a 协变。

参考答案(先自己写再看)参考答案(先自己写再看)
能否编译原因
A set(&cell, &s);Cell<T>T 不变&Cell<&'static str> 不能当作 &Cell<&'a str>;若允许,set 会把短生命周期的 &s 写进 cell,产生悬垂引用(E0597)
B let r: &str = "static";字面量是 &'static str'static 比任何 'a 都长,协变允许「长 → 短」
C let short: &str = r;与 B 同一规则:&'a T'a 协变
D m = short;short 来自字面量,实际生命周期仍是 'static;把 &'static str 赋给 &'static str 完全类型匹配

完整可编译的对照代码(把 A 换成合法形式后运行):

rust
use std::cell::Cell;

// Cell<&'static str> 只能接受 'static 的值
fn set_static(cell: &Cell<&'static str>, value: &'static str) {
    cell.set(value);
}

// 需要「任意生命周期」时,容器必须写成泛型参数,而不是靠协变
fn set_any<'a>(cell: &Cell<&'a str>, value: &'a str) {
    cell.set(value);
}

fn main() {
    let static_cell: Cell<&'static str> = Cell::new("static");
    set_static(&static_cell, "another static");
    println!("{}", static_cell.get());

    let local = String::from("local");
    let local_cell: Cell<&str> = Cell::new(&local);
    set_any(&local_cell, &local);
    println!("{}", local_cell.get());

    let r: &str = "static";
    let short: &str = r;
    let mut m: &'static str = "x";
    m = short_like(); // 见下方说明
    println!("{m}");
}

fn short_like() -> &'static str {
    "from function"
}

输出:

another static
local
from function

要点解析

  • 「不变」就是「既不能放宽也不能收紧」。Cellset(&self, value: T) 是内部可变性的典型形态: 它能在只读借用下改写内容,因此必须禁止对 T 做任何子类型替换。
  • 对比记忆:&'a mut T'a 协变、对 T 不变;Cell<T>/RefCell<T>T 不变; &'a TBox<T>Vec<T> 对各自参数协变。
  • 实战推论:需要「容器里存任意短生命周期引用」时,用生命周期参数把容器泛型化Cell<&'a str> + fn set_any<'a>),而不是指望协变帮你转换。

练习 8:Drop 的执行顺序

难度:★★☆ 要求:写出下面程序的完整输出(-- flag -- 之前与之后):

rust
struct Noisy(&'static str);
impl Drop for Noisy {
    fn drop(&mut self) { println!("drop {}", self.0); }
}
struct Pair { first: Noisy, second: Noisy }
fn main() {
    let outer = Noisy("outer");
    let pair = Pair { first: Noisy("first"), second: Noisy("second") };
    println!("-- flag --");
    std::mem::drop(pair);
    println!("-- end --");
    let _ = &outer;
}

提示:字段按声明顺序、局部变量按逆序、mem::drop 立即析构。

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

完整输出:

-- flag --
drop first
drop second
-- end --
drop outer

推导过程

  1. std::mem::drop(pair)pair 的所有权移进 drop,函数返回时立即析构。 析构结构体时,字段按声明顺序:先 first,再 second
  2. 之后 -- end -- 打印。
  3. main 结束时,局部变量按声明逆序析构:pair 已经析构过, 于是只剩 outer(它声明在 pair 之前,但逆序时排在后面)。

可运行版本:

rust
struct Noisy(&'static str);

impl Drop for Noisy {
    fn drop(&mut self) {
        println!("drop {}", self.0);
    }
}

struct Pair {
    first: Noisy,
    second: Noisy,
}

fn main() {
    let outer = Noisy("outer");
    let pair = Pair {
        first: Noisy("first"),
        second: Noisy("second"),
    };
    println!("-- flag --");
    std::mem::drop(pair);
    println!("-- end --");
    let _ = &outer;
}

输出:

-- flag --
drop first
drop second
-- end --
drop outer

要点解析

  • 字段正向、局部变量逆向——这是最容易记混的一对规则。
  • std::mem::drop 并不是什么特殊钩子,它只是 pub fn drop<T>(_x: T) {}:接收所有权后什么都不做, 参数在函数末尾被析构。
  • 如果 Pair 自己实现了 Drop,就不能再部分移动它的字段(let x = pair.first 会报 E0509), 因为 Drop::drop 仍需要访问完整的 self。这就是 Option::take 存在的理由。

练习 9:From / TryFrom 与错误转换

难度:★★☆ 要求:实现 impl From<Celsius> for Fahrenheitimpl TryFrom<&str> for Celsius (解析失败返回 ParseFloatError),并写一个函数 report(kind: &str) -> Result<String, String>"36.6C" 这样的输入转成华氏度字符串,错误信息用中文。 提示? 不能直接把 ParseFloatError 变成 String,用 map_err

参考答案(先自己写再看)参考答案(先自己写再看)
rust
struct Celsius(f64);
struct Fahrenheit(f64);

impl From<Celsius> for Fahrenheit {
    fn from(c: Celsius) -> Self {
        Fahrenheit(c.0 * 9.0 / 5.0 + 32.0)
    }
}

impl TryFrom<&str> for Celsius {
    type Error = std::num::ParseFloatError;
    fn try_from(text: &str) -> Result<Self, Self::Error> {
        text.trim().parse().map(Celsius)
    }
}

// 把 "36.6C" 转成华氏度;错误用 String 表达,便于给用户读
fn report(input: &str) -> Result<String, String> {
    let digits = input
        .strip_suffix('C')
        .ok_or_else(|| format!("缺少单位 C:{input}"))?;
    let celsius = Celsius::try_from(digits).map_err(|err| format!("无法解析温度:{err}"))?;
    let fahrenheit: Fahrenheit = celsius.into();
    Ok(format!("{:.1}F", fahrenheit.0))
}

fn main() {
    println!("{:?}", report("36.6C"));
    println!("{:?}", report("100C"));
    println!("{:?}", report("36.6F"));
    println!("{:?}", report("abcC"));
}

输出:

Ok("97.9F")
Ok("212.0F")
Err("缺少单位 C:36.6F")
Err("无法解析温度:invalid float literal")

要点解析

  • From不会失败的转换,TryFrom 才允许失败,且必须声明 type Error
  • 实现 From<Celsius> for Fahrenheit 后,Into 由标准库的毯式实现 impl<T, U: From<T>> Into<U> for T 免费提供。
  • ? 需要错误类型可转换(From)。ParseFloatError 不能自动变成 String, 所以用 map_err 转一次;如果你定义了自己的错误类型并实现 From<ParseFloatError>,就可以直接 ?
  • 不要用 unwrap() 处理用户输入:这里返回 Result 让调用者决定怎么报错。

练习 10:trait 对象的跨线程共享

难度:★★★ 要求:实现 trait Draw { fn draw(&self) -> String; } + trait Named: Draw。 用 Vec<Box<dyn Draw>> 装两个不同实现并遍历打印;再用 Arc<dyn Draw + Send + Sync> 把其中一个实现交给新线程,join 后打印结果。 提示Arc::clone + std::thread::spawn(move || ...)

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

trait Draw {
    fn draw(&self) -> String;
    fn describe(&self) -> String {
        format!("[{}]", self.draw())
    }
}

// 只继承接口,不继承数据
trait Named: Draw {
    fn name(&self) -> &'static str;
}

struct Button;

impl Draw for Button {
    fn draw(&self) -> String {
        "button".to_string()
    }
}

impl Named for Button {
    fn name(&self) -> &'static str {
        "ok"
    }
}

struct Label(String);

impl Draw for Label {
    fn draw(&self) -> String {
        format!("label({})", self.0)
    }
}

fn render_all(items: &[Box<dyn Draw>]) -> Vec<String> {
    items.iter().map(|item| item.describe()).collect()
}

fn main() {
    // 异构集合:静态派发做不到这件事
    let widgets: Vec<Box<dyn Draw>> = vec![Box::new(Button), Box::new(Label("hi".into()))];
    println!("{:?}", render_all(&widgets));

    // 借用形式:零分配
    let button = Button;
    let borrowed: &dyn Draw = &button;
    println!("{}", borrowed.draw());

    // 跨线程共享:必须显式声明 Send + Sync
    let shared: Arc<dyn Named + Send + Sync> = Arc::new(Button);
    let clone = Arc::clone(&shared);
    let handle = std::thread::spawn(move || format!("{} {}", clone.name(), clone.draw()));
    println!("{}", handle.join().unwrap());
    println!("{}", shared.describe());
}

输出(线程输出与主线程顺序确定,因为 join 在打印之前):

["[button]", "[label(hi)]"]
button
ok button
[button]

要点解析

  • Box::new(Button) 会把 Button 装箱并擦除类型,得到胖指针(数据指针 + vtable 指针)。
  • trait 可以 dyn 兼容的前提是所有方法都不涉及泛型参数、Self 返回/参数、关联常量 (drawdescribename 都满足)。
  • Arc<dyn Named + Send + Sync>dyn 会抹掉具体类型,Send/Sync 必须显式写出, 否则 thread::spawn 会拒绝这个 future/闭包的捕获值。
  • describeDraw 的默认方法,通过 Named 的 supertrait 关系依然可调用。

练习 11:用 GAT 实现借出型迭代器

难度:★★☆ 要求:实现 trait LendingIterator,它包含 type Item<'a> where Self: 'a;fn next(&mut self) -> Option<Self::Item<'_>>; 两个成员; 为一个「按固定宽度切分 &'a [T]」的结构体实现它,并用 while let 打印每段。 提示type Item<'b> = &'b [T] where Self: 'b;

参考答案(先自己写再看)参考答案(先自己写再看)
rust
trait LendingIterator {
    type Item<'a>
    where
        Self: 'a;

    fn next(&mut self) -> Option<Self::Item<'_>>;
}

struct Chunks<'a, T> {
    rest: &'a [T],
    width: usize,
}

impl<'a, T> Chunks<'a, T> {
    fn new(slice: &'a [T], width: usize) -> Self {
        Chunks { rest: slice, width: width.max(1) }
    }
}

impl<'a, T> LendingIterator for Chunks<'a, T> {
    type Item<'b> = &'b [T]
    where
        Self: 'b;

    fn next(&mut self) -> Option<Self::Item<'_>> {
        if self.rest.is_empty() {
            return None;
        }
        let rest = self.rest;
        let take = self.width.min(rest.len());
        let (head, tail) = rest.split_at(take);
        self.rest = tail;
        Some(head)
    }
}

fn main() {
    let data = [1, 2, 3, 4, 5, 6, 7];
    let mut chunks = Chunks::new(&data, 3);
    while let Some(chunk) = chunks.next() {
        println!("{chunk:?}");
    }

    // 对比:普通 Iterator 无法让 Item 借用 &mut self
    let borrowed_ok = data.iter().map(|n| n * 2).collect::<Vec<_>>();
    println!("{borrowed_ok:?}");
}

输出:

[1, 2, 3]
[4, 5, 6]
[7]
[2, 4, 6, 8, 10, 12, 14]

要点解析

  • where Self: 'a 是 GAT 的必备条件:它保证 Self 活得比借出的 'a 长, 否则 Item<'a> 可能引用已经失效的数据。
  • let rest = self.rest;&'a [T]复制&TCopy), 这样 self.rest = tailhead 的生命周期互不干扰。
  • 借出型迭代器无法用于 for 循环(for 需要 IntoIterator + Iterator), 必须手写 while let;这是它与普通 Iterator 的核心取舍。

本章小结 / 自测清单

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