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 教程目录

✈️💬