练习与自测
本章练习共 10 题,答案折叠在每题下方。建议先自己写、编译通过后再展开答案;难度标记:★☆☆ 基础 / ★★☆ 综合 / ★★★ 挑战。
代码按
rustc --edition 2024编译,可直接放进单个文件。
练习 1:泛型栈与 FromIterator
难度:★★☆ 要求:实现泛型 Stack<T>,支持 new、push、pop、peek、len、is_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: Copy或Clone。FromIterator<T>的泛型参数是T而不是Self;collect()的目标类型由标注决定。- 常见坑:同一表达式里同时
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: Clone,copy.id()需要T::Id: Debug,item.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> 的实现里先 map 再 join。
参考答案(先自己写再看)
参考答案(先自己写再看)
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
要点解析:
- 空
Vec的summarize返回空字符串,join对空集合就是这个语义,不需要特判。 - 这种「容器转发到元素」的实现完全符合孤儿规则吗?符合——
Summary是本 crate 定义的 trait, 所以可以为外部类型Vec<T>实现(条件里不涉及外部类型参数即可)。 - 注意递归实现的边界:如果将来加
impl Summary for String,Vec<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 复制出来(&str 是 Copy)避免借用冲突。
参考答案(先自己写再看)
参考答案(先自己写再看)
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
要点解析:
- 「不变」就是「既不能放宽也不能收紧」。
Cell的set(&self, value: T)是内部可变性的典型形态: 它能在只读借用下改写内容,因此必须禁止对T做任何子类型替换。 - 对比记忆:
&'a mut T对'a协变、对T不变;Cell<T>/RefCell<T>对T不变;&'a T、Box<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推导过程:
std::mem::drop(pair)把pair的所有权移进drop,函数返回时立即析构。 析构结构体时,字段按声明顺序:先first,再second。- 之后
-- end --打印。 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 Fahrenheit 与 impl 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返回/参数、关联常量 (draw、describe、name都满足)。 Arc<dyn Named + Send + Sync>:dyn会抹掉具体类型,Send/Sync必须显式写出, 否则thread::spawn会拒绝这个 future/闭包的捕获值。describe是Draw的默认方法,通过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]的复制(&T是Copy), 这样self.rest = tail与head的生命周期互不干扰。- 借出型迭代器无法用于
for循环(for需要IntoIterator+Iterator), 必须手写while let;这是它与普通Iterator的核心取舍。