练习题与解答汇总
本文件汇总所有章节的练习题和解答
建议: 先自己做, 遇到困难再看解答
章节索引
| 章节 | 练习数 | 难度分布 |
|---|---|---|
| 01-从Go到Rust-思维转换 | 5 | 基础×3 中等×2 |
| 02-所有权与内存模型 | 10 | 基础×4 中等×4 挑战×2 |
| 03-借用检查器原理 | 12 | 基础×4 中等×5 挑战×3 |
| 04-类型系统与Trait | 9 | 基础×3 中等×3 挑战×3 |
| 05-并发模型深度解析 | 10 | 基础×3 中等×4 挑战×3 |
| 06-Unsafe与链表实现 | 8 | 基础×2 中等×3 挑战×3 |
| 07-智能指针与内存管理 | 8 | 基础×2 中等×4 挑战×2 |
| 08-宏系统与元编程 | 6 | 基础×2 中等×2 挑战×2 |
| 09-异步编程与Runtime | 10 | 基础×3 中等×4 挑战×3 |
| 合计 | 78 |
章节 1: 从Go到Rust-思维转换
练习 1.1: 内存模型 (基础)
题目: 以下代码能编译吗?如果不能,如何修复?
fn main() {
let s = String::from("hello");
let t = s;
println!("{}", s); // 错误?
}答案:
// 不能编译。s 的所有权移动给了 t,之后 s 不可用。
// 修复方案 1: 克隆
fn main() {
let s = String::from("hello");
let t = s.clone();
println!("{}", s); // ok
}
// 修复方案 2: 借用
fn main() {
let s = String::from("hello");
let t = &s; // 借用,不移动
println!("{}", s); // ok
println!("{}", t); // ok
}练习 1.2: 错误处理对比 (基础)
题目: 将以下 Go 代码用惯用 Rust 改写:
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, fmt.Errorf("division by zero")
}
return a / b, nil
}答案:
fn divide(a: f64, b: f64) -> Result<f64, String> {
if b == 0.0 {
Err("division by zero".to_string())
} else {
Ok(a / b)
}
}
// 更惯用的方式 (使用 thiserror)
use thiserror::Error;
#[derive(Error, Debug)]
enum MathError {
#[error("division by zero")]
DivisionByZero,
}
fn divide_typed(a: f64, b: f64) -> Result<f64, MathError> {
if b == 0.0 {
Err(MathError::DivisionByZero)
} else {
Ok(a / b)
}
}练习 1.3: 并发模型 (基础)
题目: 实现一个 Rust 版本的 goroutine 并发求和:
// Go 版本
func parallelSum(nums []int) int {
ch := make(chan int)
go func() { ch <- sum(nums[:len(nums)/2]) }()
go func() { ch <- sum(nums[len(nums)/2:]) }()
return <-ch + <-ch
}答案:
use std::thread;
fn parallel_sum(nums: Vec<i32>) -> i32 {
let mid = nums.len() / 2;
let (left, right) = nums.split_at(mid);
let left = left.to_vec();
let right = right.to_vec();
let left_handle = thread::spawn(move || left.iter().sum::<i32>());
let right_handle = thread::spawn(move || right.iter().sum::<i32>());
left_handle.join().unwrap() + right_handle.join().unwrap()
}练习 1.4: Null 处理 (中等)
题目: 实现一个安全的链式操作:
struct User {
name: String,
address: Option<Address>,
}
struct Address {
city: Option<String>,
}
// 获取城市名称 (可能不存在)
fn get_city(user: &User) -> Option<&str> {
// 实现这个函数,不使用 match
todo!()
}答案:
fn get_city(user: &User) -> Option<&str> {
user.address.as_ref()?.city.as_deref()
}
// 等价的 match 版本
fn get_city_match(user: &User) -> Option<&str> {
match &user.address {
None => None,
Some(addr) => match &addr.city {
None => None,
Some(city) => Some(city.as_str()),
}
}
}练习 1.5: 类型转换 (中等)
题目: 实现 From/Into trait 用于错误类型转换:
#[derive(Debug)]
enum AppError {
Io(std::io::Error),
Parse(std::num::ParseIntError),
Custom(String),
}
fn parse_config(content: &str) -> Result<i32, AppError> {
// 实现: 解析字符串中的数字,可能是 IO 错误或解析错误
todo!()
}答案:
impl From<std::io::Error> for AppError {
fn from(e: std::io::Error) -> Self {
AppError::Io(e)
}
}
impl From<std::num::ParseIntError> for AppError {
fn from(e: std::num::ParseIntError) -> Self {
AppError::Parse(e)
}
}
fn parse_config(content: &str) -> Result<i32, AppError> {
let trimmed = content.trim();
let num = trimmed.parse::<i32>()?; // ? 自动调用 From<ParseIntError>
Ok(num)
}章节 2: 所有权与内存模型
练习 2.1: 找错误 (基础)
题目: 找出以下代码的问题并修复:
fn first_word(s: String) -> &str {
let bytes = s.as_bytes();
for (i, &byte) in bytes.iter().enumerate() {
if byte == b' ' {
return &s[..i];
}
}
&s
}答案:
// 问题: 返回值生命周期与参数不匹配,s 是 String (拥有所有权)
// 返回 &str 时,s 在函数结束后被 drop,引用悬空
// 修复方案 1: 接受 &str 引用
fn first_word(s: &str) -> &str {
let bytes = s.as_bytes();
for (i, &byte) in bytes.iter().enumerate() {
if byte == b' ' {
return &s[..i];
}
}
s
}
// 修复方案 2: 返回 String
fn first_word_owned(s: String) -> String {
s.split_whitespace()
.next()
.unwrap_or("")
.to_string()
}练习 2.2: Move 语义 (基础)
题目: 解释为什么能编译,并说明内存发生了什么:
fn main() {
let x = 5;
let y = x; // (1)
println!("{} {}", x, y); // (2) 为什么 x 还能用?
let s1 = String::from("hello");
let s2 = s1; // (3)
// println!("{}", s1); // (4) 为什么不能用?
}答案:
(1) x 是 i32,实现了 Copy trait
Copy 类型在赋值时复制栈上的位,不移动所有权
(2) x 和 y 是独立的栈副本,都可以使用
(3) String 没有实现 Copy(堆数据需要复杂管理)
s1 的所有权移动给 s2
(4) s1 已经 move 给 s2,s1 的使用被借用检查器禁止
防止双重释放 (double-free)
练习 2.3: RAII (基础)
题目: 实现一个计时 guard:
struct TimerGuard {
name: String,
start: std::time::Instant,
}
impl TimerGuard {
fn new(name: &str) -> Self { todo!() }
}
// 当 guard drop 时,自动打印经过时间答案:
struct TimerGuard {
name: String,
start: std::time::Instant,
}
impl TimerGuard {
fn new(name: &str) -> Self {
TimerGuard {
name: name.to_string(),
start: std::time::Instant::now(),
}
}
}
impl Drop for TimerGuard {
fn drop(&mut self) {
println!("[{}] elapsed: {:?}", self.name, self.start.elapsed());
}
}
fn main() {
let _t = TimerGuard::new("my_operation");
// 做一些事情
std::thread::sleep(std::time::Duration::from_millis(100));
// guard 在这里 drop,打印时间
}练习 2.4-2.10
详细答案见各章节文件中的对应练习。
核心考点: Drop 顺序、自定义 Copy、连接池 RAII、内存布局分析等。
章节 3: 借用检查器原理
练习 3.1: 经典错误 (基础)
题目: 修复以下代码:
fn main() {
let mut v = vec![1, 2, 3];
let first = &v[0];
v.push(4); // 错误行
println!("{}", first);
}答案:
fn main() {
let mut v = vec![1, 2, 3];
// 方案 1: 重排序,先用 first,再 push
let first = &v[0];
println!("{}", first); // 先使用
v.push(4); // 再修改
// 方案 2: 复制值而非借用
let first = v[0]; // i32 是 Copy
v.push(4);
println!("{}", first);
}原因: v.push(4) 可能触发 Vec 扩容 (重新分配内存),使 first 悬空。
练习 3.2: 生命周期标注 (基础)
题目: 为以下函数添加生命周期标注:
fn longer(s1: &str, s2: &str) -> &str {
if s1.len() > s2.len() { s1 } else { s2 }
}答案:
fn longer<'a>(s1: &'a str, s2: &'a str) -> &'a str {
if s1.len() > s2.len() { s1 } else { s2 }
}
// 解释:
// 'a 表示返回值的生命周期
// 不能比 s1 和 s2 中较短的那个长
// 因为编译器不知道会返回哪个练习 3.3-3.12
核心考点: NLL优化、结构体生命周期、内部可变性、错误理解等。
章节 4: 类型系统与Trait
练习 4.1: 实现 Trait (基础)
题目: 为 Circle 和 Rectangle 实现 Area trait:
trait Area {
fn area(&self) -> f64;
fn is_large(&self) -> bool {
self.area() > 100.0 // 默认实现
}
}
struct Circle { radius: f64 }
struct Rectangle { width: f64, height: f64 }答案:
impl Area for Circle {
fn area(&self) -> f64 {
std::f64::consts::PI * self.radius * self.radius
}
}
impl Area for Rectangle {
fn area(&self) -> f64 {
self.width * self.height
}
// is_large 使用默认实现
}
fn main() {
let c = Circle { radius: 6.0 };
let r = Rectangle { width: 5.0, height: 30.0 };
println!("Circle area: {:.2}", c.area());
println!("Rectangle is large: {}", r.is_large());
}练习 4.2-4.9
核心考点: 静态/动态分发、关联类型、泛型约束、Builder模式等。
章节 5: 并发模型
练习 5.1: 基础并发 (基础)
题目: 用多线程计算数组各元素的平方和:
fn parallel_square_sum(data: &[i64]) -> i64 {
// 使用线程并行计算
todo!()
}答案:
use std::thread;
fn parallel_square_sum(data: &[i64]) -> i64 {
let chunk_size = (data.len() + 3) / 4; // 分为4块
let handles: Vec<_> = data.chunks(chunk_size)
.map(|chunk| {
let chunk = chunk.to_vec();
thread::spawn(move || {
chunk.iter().map(|&x| x * x).sum::<i64>()
})
})
.collect();
handles.into_iter().map(|h| h.join().unwrap()).sum()
}练习 5.2-5.10
核心考点: Arc/Mutex、Channel、死锁避免、读写锁、原子操作等。
章节 6: Unsafe 与链表
练习 6.1: 裸指针 (基础)
// 使用裸指针实现 swap
unsafe fn raw_swap<T>(a: *mut T, b: *mut T) {
let temp = std::ptr::read(a);
std::ptr::write(a, std::ptr::read(b));
std::ptr::write(b, temp);
}
fn main() {
let mut x = 1i32;
let mut y = 2i32;
unsafe { raw_swap(&mut x, &mut y); }
assert_eq!((x, y), (2, 1));
}练习 6.2-6.8
核心考点: MyBox实现、迭代器、LRU测试、Vec实现等。
章节 7: 智能指针
练习 7.1 解答
// 已在章节文件中给出完整实现
// SimpleRc<T> 的实现包含: new, clone, deref, drop章节 8: 宏系统
练习 8.1: assert_approx_eq! (基础)
macro_rules! assert_approx_eq {
($a:expr, $b:expr) => { assert_approx_eq!($a, $b, 1e-6) };
($a:expr, $b:expr, $eps:expr) => {
{
let (a, b, eps) = ($a, $b, $eps);
if (a - b).abs() > eps {
panic!("assertion failed: ({:?} ≈ {:?}) within {:?}", a, b, eps);
}
}
};
}章节 9: 异步编程
练习 9.1: 并发请求 (基础)
// 使用 futures::future::join_all 并发执行多个 Future
// 详细实现见章节文件练习 9.4: Actor 模式 (中等)
// Actor 通过 mpsc channel 接收消息
// 所有状态变更通过消息序列化,天然线程安全
// 详细实现见章节文件快速检测题
基础知识
-
问:
String和&str的区别? 答:String是拥有所有权的堆分配字符串;&str是字符串切片引用,可以指向 String、字面量或任意 UTF-8 内存。 -
问: 什么类型默认实现了
Copy? 答: 所有基本数值类型 (i32, f64, bool, char 等)、不可变引用&T、由 Copy 类型组成的元组和数组。 -
问:
Rc<T>vsArc<T>? 答:Rc使用非原子引用计数,只能在单线程使用;Arc使用原子操作,可跨线程共享。Arc有额外的原子操作开销。 -
问: 什么是
Send和Sync? 答:Send表示类型可以安全地转移到另一个线程;Sync表示可以安全地通过引用共享。Arc<T>要求 T: Send + Sync。 -
问:
Box<dyn Trait>vsimpl Trait的区别? 答:Box<dyn Trait>是运行时动态分发,有堆分配和 vtable 查找开销,但可以在集合中存储不同类型;impl Trait是编译时静态分发,零运行时开销,但每个具体类型会生成不同代码。
完成所有练习, 你就真正掌握了 Rust! 🦀