练习题与解答汇总

本文件汇总所有章节的练习题和解答

建议: 先自己做, 遇到困难再看解答


章节索引

章节练习数难度分布
01-从Go到Rust-思维转换5基础×3 中等×2
02-所有权与内存模型10基础×4 中等×4 挑战×2
03-借用检查器原理12基础×4 中等×5 挑战×3
04-类型系统与Trait9基础×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-异步编程与Runtime10基础×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 接收消息
// 所有状态变更通过消息序列化,天然线程安全
// 详细实现见章节文件

快速检测题

基础知识

  1. 问: String 和 &str 的区别? 答: String 是拥有所有权的堆分配字符串; &str 是字符串切片引用,可以指向 String、字面量或任意 UTF-8 内存。

  2. 问: 什么类型默认实现了 Copy? 答: 所有基本数值类型 (i32, f64, bool, char 等)、不可变引用 &T、由 Copy 类型组成的元组和数组。

  3. 问: Rc<T> vs Arc<T>? 答: Rc 使用非原子引用计数,只能在单线程使用;Arc 使用原子操作,可跨线程共享。Arc 有额外的原子操作开销。

  4. 问: 什么是 Send 和 Sync? 答: Send 表示类型可以安全地转移到另一个线程;Sync 表示可以安全地通过引用共享。Arc<T> 要求 T: Send + Sync。

  5. 问: Box<dyn Trait> vs impl Trait 的区别? 答: Box<dyn Trait> 是运行时动态分发,有堆分配和 vtable 查找开销,但可以在集合中存储不同类型;impl Trait 是编译时静态分发,零运行时开销,但每个具体类型会生成不同代码。


完成所有练习, 你就真正掌握了 Rust! 🦀