练习与自测
本章练习共 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 / f64。nums.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> 表示「可能没有」,而不是用 -1 或 0 当哨兵——后者会与实际数据混淆。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) -> i32 是 trait 约束。无捕获的闭包是零大小类型,可以强制转成函数指针;捕获了环境的闭包不行——它带有隐藏的捕获字段,只能用 impl Fn/泛型接收。这就是为什么库 API 通常写成 F: Fn(..) -> ..(能同时吃函数、闭包),只在需要「必须是无状态函数指针」(例如要存进 struct、要跨 FFI)时才用 fn(..) -> ..。