Rust 练习题(15 题)
Rust 的学习曲线比多数语言陡,光看语法不动手很容易"看懂了写不出"。这一页整理 15 道精选练习题,按难度分三级——基础 6 题(变量 / 运算 / 控制流 / 函数)、进阶 5 题(所有权 / 枚举 / 错误处理)、实战 4 题(综合应用)。每题都包含题目描述、提示、可折叠的参考答案。
使用建议:先别急着展开答案。读题后自己用 Rust Playground 或本地 cargo new 试着写,卡住了再看提示,实在写不出再对照参考答案。Rust 编译器的报错是"会说话的老师",读懂报错本身就是学习。
基础(6 题):变量 / 运算 / 控制流 / 函数
这一组覆盖 Rust 的基础语言元素,目标是用最短的代码解决一个小问题。重点体会 let 的不可变默认、类型推断、Result / Option 在错误处理中的强制作用。
第 1 题:翻转字符串
题目:写一个函数 reverse_string(s: &str) -> String,把传入的字符串翻转过来。比如 "hello" 翻转成 "olleh"。
提示:Rust 字符串是 UTF-8 字节序列,必须先 .chars() 转 Unicode 字符迭代器才能正确翻转中文;再 .rev() + .collect::<String>() 拼回 String。
参考答案
// 方法 1:chars().rev().collect::<String>() 最地道
fn reverse_string(s: &str) -> String {
s.chars().rev().collect::<String>()
}
// 方法 2:手动 for 循环(理解原理)
fn reverse_manual(s: &str) -> String {
let mut out = String::new();
for ch in s.chars().rev() {
out.push(ch);
}
out
}
fn main() {
println!("{}", reverse_string("hello")); // olleh
println!("{}", reverse_string("Rust")); // tsuR
println!("{}", reverse_manual("world")); // dlrow
}
// 输出: olleh / tsuR / dlrow第 2 题:判断素数
题目:写一个函数 is_prime(n: u32) -> bool,判断正整数 n 是否为素数。
提示:小于 2 的都不是素数;只需检查从 2 到 sqrt(n) 的整除性;(2..=limit).all(|i| n % i != 0) 用迭代器最简洁。
参考答案
fn is_prime(n: u32) -> bool {
if n < 2 { return false; }
// 只需检查到 sqrt(n):超过的因子必然配对一个更小的、已检查过的
let limit = (n as f64).sqrt() as u32;
(2..=limit).all(|i| n % i != 0)
}
fn main() {
println!("{}", is_prime(2)); // true
println!("{}", is_prime(7)); // true
println!("{}", is_prime(10)); // false
println!("{}", is_prime(1)); // false
}
// 输出: true / true / false / false第 3 题:FizzBuzz
题目:用 println! 打印 1 到 15。3 的倍数打印 Fizz,5 的倍数打印 Buzz,同时是 3 和 5 的倍数打印 FizzBuzz,其余打印数字本身。
提示:if / else if / else 的判断顺序很关键——必须先判断"同时是 3 和 5 的倍数"(即 15 的倍数);遍历用 for i in 1..=15(右闭区间)。
参考答案
fn main() {
// 经典题:3 的倍数 Fizz,5 的倍数 Buzz,15 的倍数 FizzBuzz
for i in 1..=15 {
if i % 15 == 0 { // 必须先判断 15,否则会被 3 或 5 截胡
println!("FizzBuzz");
} else if i % 3 == 0 {
println!("Fizz");
} else if i % 5 == 0 {
println!("Buzz");
} else {
println!("{}", i);
}
}
}
// 输出:
// 1
// 2
// Fizz
// 4
// Buzz
// Fizz
// 7
// 8
// Fizz
// Buzz
// 11
// Fizz
// 13
// 14
// FizzBuzz第 4 题:切片最大值
题目:写一个函数 find_max(slice: &[i32]) -> Option<i32>,返回切片中的最大值;空切片返回 None。
提示:slice.iter().copied().max() 一行搞定;.copied() 把 &i32 解引用成 i32;返回 Option 强制调用方处理空切片,杜绝"默认值"陷阱。
参考答案
// iter().max() 返回 Option<&T>:空切片返回 None,避免 panic 或"默认值"陷阱
fn find_max(slice: &[i32]) -> Option<i32> {
slice.iter().copied().max() // copied() 把 &i32 解引用成 i32
}
fn main() {
println!("{:?}", find_max(&[3, 1, 4, 1, 5, 9, 2, 6])); // Some(9)
println!("{:?}", find_max(&[-5, -2, -9])); // Some(-2)
println!("{:?}", find_max(&[])); // None
}
// 输出: Some(9) / Some(-2) / None第 5 题:求和
题目:写一个函数 sum_slice(slice: &[i32]) -> i32,返回切片元素总和;空切片返回 0。
提示:slice.iter().sum::<i32>()——turbofish 写法显式指定返回类型;空切片 sum 返回 0(零值)。
参考答案
// iter().sum::<i32>() 用 turbofish 显式指定返回类型
fn sum_slice(slice: &[i32]) -> i32 {
slice.iter().sum::<i32>() // 空切片返回 0(sum 的"零值")
}
fn main() {
println!("{}", sum_slice(&[1, 2, 3, 4, 5])); // 15
println!("{}", sum_slice(&[10, 20, 30])); // 60
println!("{}", sum_slice(&[])); // 0
}
// 输出: 15 / 60 / 0第 6 题:计算阶乘
题目:写一个函数 factorial(n: u32) -> u64,返回 n!。要求 factorial(0) == 1。
提示:最地道的写法 (1..=n).map(|x| x as u64).product::<u64>()——空 Range 的 product 返回 1;注意用 u64 承接避免溢出。
参考答案
// 方法 1:Range::product 最地道,(1..=n) 是右闭区间
fn factorial_range(n: u32) -> u64 {
(1..=n).map(|x| x as u64).product::<u64>()
}
// 方法 2:循环累乘(理解原理)
fn factorial_loop(n: u32) -> u64 {
let mut result: u64 = 1;
for i in 1..=n {
result *= i as u64;
}
result
}
fn main() {
println!("{}", factorial_range(5)); // 120
println!("{}", factorial_range(0)); // 1(空 Range 的 product 返回 1)
println!("{}", factorial_loop(6)); // 720
}
// 输出: 120 / 1 / 720进阶(5 题):所有权 / 枚举 / 错误处理
这一组覆盖 Rust 最具特色也最常"劝退"新手的概念——所有权系统。理解 move / 借用 / 生命周期是写 Rust 的核心;enum + match 是数据建模的基石;Result + ? 是错误处理的标配。做通了这组,你就算跨过了 Rust 最陡的坎。
第 7 题:所有权 move vs 借用(&)对比
题目:写三个函数演示:(a) take_ownership(s: String) 接收所有权;(b) borrow(s: &String) 只读借用;(c) push_exclamation(s: &mut String) 可变借用。
提示:被 move 的变量后续不能再用(编译错);& 是只读借用,原变量继续可用;&mut 可变借用要求变量本身用 let mut 声明。
参考答案
// 1. move:函数接收 String 所有权,原变量失效(不能再用)
fn take_ownership(s: String) {
println!("take: {}", s);
} // s 在此离开作用域,内存自动释放(drop)
// 2. 借用:函数接收 &String(只读引用),原变量继续可用
fn borrow(s: &String) -> usize {
s.len()
}
// 3. 可变借用 &mut:能修改借来的数据(但同一时刻只能有一个可变引用)
fn push_exclamation(s: &mut String) {
s.push('!');
}
fn main() {
let s1 = String::from("hello");
take_ownership(s1);
// println!("{}", s1); // 编译错!s1 已被 move,不能再用
let s2 = String::from("world");
let len = borrow(&s2); // 借用,s2 还能用
println!("s2 = {}, len = {}", s2, len);
let mut s3 = String::from("hi");
push_exclamation(&mut s3); // 可变借用
println!("{}", s3); // hi!
}
// 输出: take: hello / s2 = world, len = 5 / hi!第 8 题:String 与 &str 互转
题目:演示三种转换:(a) &str 转 String;(b) String 转 &str;(c) 函数参数用 &str 同时接收两种类型。
提示:&str 转 String 用 .to_string() 或 String::from();String 转 &str 用 &owned(deref coercion)或 .as_str();函数参数尽量用 &str 更通用。
参考答案
// String:堆上、可变、拥有所有权; &str:借用、只读(可能来自栈/堆/二进制)
// 1. &str -> String:用 to_string() 或 String::from()
fn make_string(literal: &str) -> String {
literal.to_string() // 字面量("foo")就是 &'static str
}
// 2. String -> &str:用 & 或 .as_str()(自动 deref coercion)
fn measure(s: &str) -> usize {
s.len()
}
fn main() {
let owned: String = make_string("hello");
let borrowed: &str = &owned; // String 自动 deref 成 &str
let borrowed2: &str = owned.as_str();
println!("{} / {} / {}", owned, borrowed, borrowed2);
println!("len = {}", measure(&owned)); // 传 String 给 &str 参数,自动转换
println!("len = {}", measure("world")); // 字面量直接是 &str
}
// 输出: hello / hello / hello / len = 5 / len = 5第 9 题:Vec 操作(push / iter / collect)
题目:创建一个 Vec<i32>,演示 push / remove 增删;再用 iter().map().collect() 平方所有元素;最后 filter 出 1~10 中的偶数。
提示:Vec::new() 或 vec![1, 2, 3] 宏创建;.iter().map(|x| ...).collect::<Vec<i32>>() 函数式变换;.collect() 必须有类型注解(变量类型或 turbofish)。
参考答案
fn main() {
// 1. 创建 + 增删
let mut nums: Vec<i32> = Vec::new();
nums.push(1); nums.push(2); nums.push(3); // [1, 2, 3]
nums.push(4); // [1, 2, 3, 4]
nums.remove(0); // [2, 3, 4]
println!("{:?}", nums); // [2, 3, 4]
// 2. iter().map().collect() 函数式变换
let doubled: Vec<i32> = nums.iter().map(|x| x * 2).collect();
println!("{:?}", doubled); // [4, 6, 8]
// 3. iter().filter().collect()
let evens: Vec<i32> = (1..=10).filter(|x| x % 2 == 0).collect();
println!("{:?}", evens); // [2, 4, 6, 8, 10]
// 4. vec! 宏快速初始化
let fruits = vec!["apple", "banana", "cherry"];
println!("{:?}", fruits);
}
// 输出: [2, 3, 4] / [4, 6, 8] / [2, 4, 6, 8, 10] / ["apple", "banana", "cherry"]第 10 题:enum + match(自定义枚举或 Option)
题目:定义一个 Shape 枚举(Circle / Rectangle / Triangle,各自携带不同的边长数据),写 area(s: Shape) -> f64 用 match 计算面积;再用 Option 处理"取字符串首词"的边界情况。
提示:Rust 枚举能携带数据(代数数据类型);match 必须穷尽所有变体,编译器强制;Option<T> 是标准库内置的 Some(T) | None,代替"null"。
参考答案
// 自定义枚举:Rust 枚举能"携带数据",类似代数数据类型
enum Shape {
Circle(f64), // 半径
Rectangle(f64, f64), // 宽, 高
Triangle(f64, f64, f64), // 三边
}
fn area(s: Shape) -> f64 {
match s { // match 必须穷尽所有分支,编译器强制完整
Shape::Circle(r) => std::f64::consts::PI * r * r,
Shape::Rectangle(w, h) => w * h,
Shape::Triangle(a, b, c) => {
// 海伦公式
let p = (a + b + c) / 2.0;
(p * (p - a) * (p - b) * (p - c)).sqrt()
}
}
}
// Option 是标准库内置枚举:Some(v) 或 None
fn first_word(s: &str) -> Option<&str> {
s.split_whitespace().next() // 空字符串返回 None
}
fn main() {
println!("{:.2}", area(Shape::Circle(2.0))); // 12.57
println!("{:.2}", area(Shape::Rectangle(3.0, 4.0))); // 12.00
match first_word("hello world") {
Some(w) => println!("首词: {}", w), // 首词: hello
None => println!("(空字符串)"),
}
}
// 输出: 12.57 / 12.00 / 首词: hello第 11 题:Result 错误处理(? 运算符)
题目:写 parse_and_double(s: &str) -> Result<i32, AppError>,解析字符串成数字再乘 2。要求:空字符串返回自定义错误,解析失败用 ? 传播错误。
提示:? 运算符 Ok 就解包继续、Err 就提前 return;impl From<ParseIntError> for AppError 让 ? 自动转换错误类型;错误统一成自己的 enum 是 Rust 最佳实践。
参考答案
use std::num::ParseIntError;
// 返回 Result<T, E>:Ok(v) 成功,Err(e) 失败。错误类型用枚举归一
#[derive(Debug)]
enum AppError {
Parse(ParseIntError),
Empty,
}
// From 让 ? 自动转换错误类型(ParseIntError -> AppError::Parse)
impl From<ParseIntError> for AppError {
fn from(e: ParseIntError) -> Self { AppError::Parse(e) }
}
// ? 运算符:Ok 就解包继续,Err 就提前 return(自动转换错误)
fn parse_and_double(s: &str) -> Result<i32, AppError> {
if s.is_empty() { return Err(AppError::Empty); }
let n: i32 = s.parse()?; // 出错时 return Err(自动 From 转换)
Ok(n * 2)
}
fn main() {
println!("{:?}", parse_and_double("21")); // Ok(42)
println!("{:?}", parse_and_double("abc")); // Err(Parse(...))
println!("{:?}", parse_and_double("")); // Err(Empty)
}
// 输出: Ok(42) / Err(Parse(ParseIntError { kind: InvalidDigit })) / Err(Empty)实战(4 题):综合应用
这一组综合运用 struct、Vec、HashMap、文件 IO、命令行参数——做出几个接近真实场景的小程序。Rust 的安全性和性能在这些题目里会充分体现。
第 12 题:学生管理(struct Student + Vec,排序)
题目:定义 Student 结构体(姓名 + 数学 + 语文,能算均分),用 Vec<Student> 管理多个学生,按均分降序排名。
提示:#[derive(Debug)] 让结构体可打印;impl Student { fn average(&self) -> f64 {...} } 加方法;sort_by(|a, b| b.average().partial_cmp(&a.average()).unwrap()) 浮点排序降序。
参考答案
#[derive(Debug)]
struct Student {
name: String,
math: f64,
chinese: f64,
}
impl Student {
fn average(&self) -> f64 { // &self 借用,不消耗所有权
(self.math + self.chinese) / 2.0
}
}
fn main() {
let mut students = vec![
Student { name: "小明".into(), math: 90.0, chinese: 80.0 },
Student { name: "小红".into(), math: 95.0, chinese: 88.0 },
Student { name: "小刚".into(), math: 70.0, chinese: 100.0 },
];
// sort_by 接收一个比较函数;借用元素(&Student)避免 move
students.sort_by(|a, b| b.average().partial_cmp(&a.average()).unwrap());
println!("成绩排名:");
for (i, s) in students.iter().enumerate() {
println!(" {}. {:?} 均分 {:.1}", i + 1, s.name, s.average());
}
}
// 输出:
// 成绩排名:
// 1. "小红" 均分 91.5
// 2. "小明" 均分 85.0
// 3. "小刚" 均分 85.0第 13 题:词频统计(HashMap + entry API)
题目:统计英文文本中每个单词的出现次数,按频次降序输出 Top 5。
提示:HashMap<String, u32> 计数;counts.entry(w).or_insert(0) += 1 是 entry API 的惯用法——key 不存在时插入 0,返回可变引用原地加 1;into_iter().collect() 转 Vec 后 sort_by 排序。
参考答案
use std::collections::HashMap;
fn word_count(text: &str) -> HashMap<String, u32> {
let mut counts = HashMap::new();
// split_whitespace 按空白切分,统一转小写避免 The/the 分开计数
for word in text.split_whitespace() {
let w = word.to_lowercase();
// entry API:or_insert 在 key 不存在时插入默认值(0),返回可变引用
*counts.entry(w).or_insert(0) += 1;
}
counts
}
fn main() {
let text = "the quick brown fox jumps over the lazy dog the dog";
let mut counts: Vec<_> = word_count(text).into_iter().collect();
// 按频次降序排序
counts.sort_by(|a, b| b.1.cmp(&a.1));
println!("Top 5:");
for (word, c) in counts.iter().take(5) {
println!(" {}: {}", word, c);
}
}
// 输出:
// Top 5:
// the: 3
// dog: 2
// quick: 1
// brown: 1
// fox: 1第 14 题:文件读写(std::fs::read_to_string + Result)
题目:写代码完成:(a) 整文件读;(b) 覆盖写;(c) 追加写(用 OpenOptions);(d) main 函数返回 io::Result<()> 用 ? 传播错误。
提示:fs::read_to_string(path)? 一次读全文;fs::write(path, content)? 覆盖;fs::OpenOptions::new().append(true).open(path)? 打开追加模式;main 返回 Result 让运行时打印错误。
参考答案
use std::fs;
use std::io::Write;
fn main() -> std::io::Result<()> {
// 1. 整文件读取(read_to_string 自动处理打开/关闭)
// let content = fs::read_to_string("input.txt")?;
// println!("{}", content);
// 2. 写文件
fs::write("output.txt", "第一行\n")?; // 覆盖写
// 3. 追加写(OpenOptions 控制打开模式)
let mut f = fs::OpenOptions::new()
.append(true)
.open("output.txt")?;
writeln!(f, "追加行")?;
// 4. 读取并按行处理(逐行,常量内存)
// for line in fs::read_to_string("output.txt")?.lines() {
// println!("{}", line);
// }
// 演示:写入后读回校验
let content = fs::read_to_string("output.txt")?;
println!("文件内容:\n{}", content);
Ok(()) // ? 传播错误,main 自己返回 Result 让运行时打印
}
// 输出(同时 output.txt 被写入两行):
// 文件内容:
// 第一行
// 追加行第 15 题:CLI 计算器(match 分派)
题目:用 cargo run -- 10 + 3 的形式实现命令行计算器。要求:支持 + - * /;除零和未知运算符返回 Err;参数格式错误打印用法。
提示:env::args().collect::<Vec<String>>() 取参数;parse()<f64>() 解析数字;calc 函数用 match 分派运算符;main 里 match calc(...) { Ok(r) => ..., Err(e) => ... } 分支处理结果。
参考答案
use std::env;
fn calc(a: f64, op: &str, b: f64) -> Result<f64, String> {
match op {
"+" => Ok(a + b),
"-" => Ok(a - b),
"*" => Ok(a * b),
"/" => {
if b == 0.0 { return Err("除数不能为 0".into()); }
Ok(a / b)
}
_ => Err(format!("不支持的运算符: {}", op)),
}
}
fn main() {
// 命令行用法:cargo run -- 10 + 3
let args: Vec<String> = env::args().collect();
if args.len() != 4 {
eprintln!("用法: {} <a> <op> <b>", args[0]);
return;
}
let a: f64 = match args[1].parse() {
Ok(v) => v,
Err(_) => { eprintln!("a 不是数字"); return; }
};
let op = &args[2];
let b: f64 = match args[3].parse() {
Ok(v) => v,
Err(_) => { eprintln!("b 不是数字"); return; }
};
match calc(a, op, b) {
Ok(result) => println!("{} {} {} = {}", a, op, b, result),
Err(e) => eprintln!("错误: {}", e),
}
// 非命令行演示(直接调 calc):
println!("{}", calc(3.0, "+", 5.0).unwrap()); // 8
println!("{}", calc(10.0, "/", 4.0).unwrap()); // 2.5
println!("{:?}", calc(2.0, "*", 9.0)); // Ok(18)
}
// 输出(无命令行参数时除演示外会先打印用法):
// 用法: .../calc <a> <op> <b>
// 8
// 2.5
// Ok(18)练习建议
不要只看答案。把每道题在 Rust Playground 或本地 cargo new 亲手敲一遍,改一改输入试试边界情况(空切片、零、负数、超长字符串)。Rust 编译器比任何老师都严格,每一个错误都是一次"内化所有权"的机会。
试着扩展:做完第 12 题学生管理,能不能加上"按单科排名""序列化成 JSON"?做完第 13 题词频,能不能加上停用词过滤?做完第 15 题 CLI 计算器,能不能加上幂运算?每个扩展都是一次小型的真实开发。
更多练习资源:官方互动教程 Rustlings;刷题去 LeetCode;做小项目去 GitHub 找灵感;遇到不懂的先查 Rust 语言圣经(中文社区版) 或 The Rust Book。
← 上一篇 Rust 实战项目
返回 Rust 教程目录