Skip to content

练习与自测

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

练习 2:用元组返回多个统计值

要求:返回 (min, max, avg)

参考答案(先自己写再看)
rust
fn agg(nums: &[i32]) -> (i32, i32, f64) {
    let mut min = nums[0]; // 调用方负责不传空切片(〈错误处理〉一章会讲如何优雅处理)
    let mut max = nums[0];
    let mut sum = 0;
    for &n in nums {
        // 注意 `&n`:迭代 &[i32] 得到 &i32,用模式把它解成 i32
        if n < min {
            min = n;
        }
        if n > max {
            max = n;
        }
        sum += n;
    }
    (min, max, sum as f64 / nums.len() as f64)
}

fn main() {
    println!("{:?}", agg(&[3, 1, 4, 1, 5])); // (1, 5, 2.8)
}

要点解析for &n in nums 里的 &n 是模式解构,等价于 for item in nums { let n = *item; }sum as f64 是必需的——Rust 不允许 i32 / f64nums.len()usize,也要转成 f64

练习 3:用 match 实现分数段判断

要求match 分数段。注意 >100 不能漏。

参考答案(先自己写再看)
rust
fn grade(score: u32) -> char {
    match score {
        90..=100 => 'A',
        80..=89 => 'B',
        70..=79 => 'C',
        60..=69 => 'D',
        0..=59 => 'F',
        _ => '?', // u32 到 u32::MAX 的剩余情况必须有人兜底
    }
}

fn main() {
    for s in [100, 85, 72, 60, 0, 200] {
        print!("{s}:{} ", grade(s));
    }
    println!(); // 100:A 85:B 72:C 60:D 0:F 200:?
}

要点解析:范围模式 a..=b 是闭区间。用 u32_ 分支是不可省略的(编译器会报 non-exhaustive)。如果分数改成语义受限的 newtype(〈宏〉一章),就能把非法值挡在类型之外。

练习 4:只用 match 实现 FizzBuzz

要求:只用 match 做 FizzBuzz 判定。

参考答案(先自己写再看)
rust
fn classify(n: u32) -> &'static str {
    // 对「元组模式」做匹配:一次性看两个条件,避免嵌套 if
    match (n % 3, n % 5) {
        (0, 0) => "FizzBuzz",
        (0, _) => "Fizz",
        (_, 0) => "Buzz",
        _ => "plain",
    }
}

fn main() {
    let line: Vec<String> = (1..=15)
        .map(|n| match classify(n) {
            "plain" => n.to_string(),
            word => word.to_string(),
        })
        .collect();
    println!("{}", line.join(" "));
    // 1 2 Fizz 4 Buzz Fizz 7 8 Fizz Buzz 11 Fizz 13 14 FizzBuzz
}

要点解析:把两个取模结果打包成元组再 match,是 Rust 消除嵌套 if 的常用手法(和 (a, b) 条件表同构)。&'static str 能返回是因为这些字面量存活于整个程序。

练习 6:match 计算器与 Result

要求match 计算器,返回 Result<f64, String>

参考答案(先自己写再看)
rust
fn calc(op: char, a: f64, b: f64) -> Result<f64, String> {
    match op {
        '+' => Ok(a + b),
        '-' => Ok(a - b),
        '*' => Ok(a * b),
        '/' if b == 0.0 => Err(String::from("除数不能为 0")), // 守卫先拦住危险分支
        '/' => Ok(a / b),
        other => Err(format!("不支持的运算符:{other}")),
    }
}

fn main() {
    println!("{:?}", calc('+', 1.0, 2.0)); // Ok(3.0)
    println!("{:?}", calc('/', 1.0, 0.0)); // Err("除数不能为 0")
    println!("{:?}", calc('^', 2.0, 3.0)); // Err("不支持的运算符:^")

    // 也可以用 match 展开 Result
    match calc('*', 6.0, 7.0) {
        Ok(v) => println!("= {v}"),
        Err(e) => println!("错误:{e}"),
    }
}

要点解析'/' if b == 0.0带守卫的分支,必须写在普通 '/' 分支之前,否则永远不可达。返回 Result 而不是 panic!,是把「失败」当作值交给调用方(〈错误处理〉一章会讲 ?)。这里用 String 当错误类型是为了教学直观;库代码更该定义自己的错误 enum

练习 7:变量遮蔽计算矩形面积

要求Rect + 拓宽计算面积。

参考答案(先自己写再看)
rust
struct Rect {
    width: u32,
    height: u32,
}

fn area(r: &Rect) -> u64 {
    // u32 * u32 可能溢出到 u32 之外,先无损拓宽到 u64
    u64::from(r.width) * u64::from(r.height)
}

fn main() {
    let r = Rect { width: 100_000, height: 100_000 };
    println!("{}", area(&r)); // 10000000000

    // 反例:u32 直接相乘会在 debug 下 panic
    // let bad = r.width * r.height;
}

要点解析u64::from(u32)保证成功的拓宽转换(From),比 as u64 更明确地表达「这里不丢数据」。参数用 &Rect 而不是 Rect,避免移动结构体。

练习 8:遍历切片做条件统计

要求:统计非负个数与最大正数。

参考答案(先自己写再看)
rust
fn stats(data: &[i32]) -> (usize, Option<i32>) {
    let mut non_negative = 0usize;
    let mut max_positive: Option<i32> = None;

    for &v in data {
        if v >= 0 {
            non_negative += 1;
        }
        // `is_none_or`:None 时算「满足」,Some(m) 时比较
        if v > 0 && max_positive.is_none_or(|m| v > m) {
            max_positive = Some(v);
        }
    }
    (non_negative, max_positive)
}

fn main() {
    println!("{:?}", stats(&[-1, 2, 3, 0])); // (3, Some(3))
    println!("{:?}", stats(&[-5, -1])); // (0, None)

    // 用 if let 取出结果,避免 unwrap
    let (count, best) = stats(&[4, -2, 9]);
    if let Some(m) = best {
        println!("{count} 个非负,最大正数 {m}");
    } else {
        println!("{count} 个非负,没有正数");
    }
}

要点解析:用 Option<i32> 表示「可能没有」,而不是用 -10 当哨兵——后者会与实际数据混淆。max_positive.is_none_or(|m| v > m) 等同于 match max_positive { None => true, Some(m) => v > m }

练习 9:账本结算与饱和加法

要求:账本余额 + 未知操作跳过 + 饱和加法。

参考答案(先自己写再看)
rust
fn bank(ops: &[(&str, i64)]) -> i64 {
    let mut balance = 0i64;
    for &(kind, amount) in ops {
        // match 作为表达式产出「金额变化量」;不支持的操作用 continue 提前跳过本条
        let delta = match kind {
            "deposit" => amount,
            "withdraw" => -amount,
            _ => {
                println!("忽略未知操作:{kind}");
                continue; // continue 的类型是 !,所以这个分支能当 i64 用
            }
        };
        // 饱和加法:即使余额被推到 i64::MAX/MIN 也不会 panic 或环绕
        balance = balance.saturating_add(delta);
    }
    balance
}

fn main() {
    let ops = [("deposit", 100), ("withdraw", 30), ("nope", 5)];
    println!("{}", bank(&ops)); // 打印一行提示后输出 70

    let huge = [("deposit", i64::MAX), ("deposit", 10)];
    println!("{}", bank(&huge)); // 9223372036854775807(饱和)
}

要点解析:这是本章综合题,串起了 match 表达式、continue 的发散类型 !、模式解构 &(kind, amount)、以及 saturating_add。如果写成 balance += delta,debug 下溢出会 panic——金融场景宁可饱和/报错也不要静默环绕。

练习 10:函数指针作为参数

要求:函数指针作为参数。

参考答案(先自己写再看)
rust
fn triple(v: i32) -> i32 {
    v * 3
}

fn apply_all(f: fn(i32) -> i32, data: &[i32]) -> Vec<i32> {
    let mut out = Vec::with_capacity(data.len());
    for &v in data {
        out.push(f(v));
    }
    out
}

fn main() {
    println!("{:?}", apply_all(triple, &[1, 2, 3])); // [3, 6, 9]

    // 函数指针也能用泛型参数接受闭包:F: Fn(i32) -> i32
    fn apply_all_generic<F: Fn(i32) -> i32>(f: F, data: &[i32]) -> Vec<i32> {
        data.iter().map(|&v| f(v)).collect()
    }
    let offset = 10;
    println!("{:?}", apply_all_generic(|v| v + offset, &[1, 2]));
    // [11, 12]:闭包捕获了 offset

    // 无捕获的闭包也能传给 fn 指针参数
    println!("{:?}", apply_all(|v| v - 1, &[1, 2])); // [0, 1]
}

要点解析fn(i32) -> i32函数指针类型,Fn(i32) -> i32trait 约束。无捕获的闭包是零大小类型,可以强制转成函数指针;捕获了环境的闭包不行——它带有隐藏的捕获字段,只能用 impl Fn/泛型接收。这就是为什么库 API 通常写成 F: Fn(..) -> ..(能同时吃函数、闭包),只在需要「必须是无状态函数指针」(例如要存进 struct、要跨 FFI)时才用 fn(..) -> ..

本章小结 / 自测清单

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