Rust 实战项目

学了 16 篇基础,是时候把它们串起来做点真东西了。这一章给三个完整、可编译运行的项目——CLI 待办管理、多线程 HTTP 请求器、简易 grep,每个都从需求出发,给出完整代码、运行结果和要点讲解。三个项目分别对应 Rust 最常被使用的三个方向:应用开发(命令行工具)、系统编程(并发与网络)、文本处理(迭代器组合)。建议你用 cargo new 把每个项目在本地亲手跑一遍,再改一改需求,这才是真正"上手"的感觉。

实战 1:CLI 待办管理(struct + Vec + serde_json)

需求:做一个待办管理 CLI,支持三个子命令——add 添加、list 列出、done 标记完成,数据存 JSON 文件。展示 Rust 的所有权(数据 move 进 Vec、借用遍历)和 Result 错误处理实战。

涉及知识:struct 定义、Vec 操作、serde 序列化、文件 I/O、Result 与 map_err、iter / iter_mut 借用、env::args 命令行参数。先建项目并加依赖:cargo new todo,在 Cargo.toml 加 serde 和 serde_json。

// src/main.rs — CLI 待办管理(add / list / done)
// 依赖(Cargo.toml):
//   [dependencies]
//   serde = { version = "1", features = ["derive"] }
//   serde_json = "1"
//
// 运行(用 cargo run -- 传参):
//   cargo run -- add 买牛奶
//   cargo run -- list
//   cargo run -- done 1
use serde::{Deserialize, Serialize};
use std::env;
use std::fs;
use std::process;

const TODO_FILE: &str = "todos.json";

// derive 宏自动生成序列化/反序列化代码,零成本抽象
#[derive(Serialize, Deserialize, Debug)]
struct Todo {
    id: u32,
    title: String,
    done: bool,
}

// 从文件加载;文件不存在视为空列表(Result 错误处理实战)
fn load() -> Result<Vec<Todo>, String> {
    match fs::read_to_string(TODO_FILE) {
        Ok(text) => {
            let todos: Vec<Todo> = serde_json::from_str(&text)
                .map_err(|e| format!("解析失败: {}", e))?;
            Ok(todos)
        }
        Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(Vec::new()),
        Err(e) => Err(format!("读取失败: {}", e)),
    }
}

// 保存到文件(借用 &[Todo],不获取所有权)
fn save(todos: &[Todo]) -> Result<(), String> {
    let text = serde_json::to_string_pretty(todos)
        .map_err(|e| format!("序列化失败: {}", e))?;
    fs::write(TODO_FILE, text).map_err(|e| format!("写入失败: {}", e))
}

fn cmd_add(args: &[String]) -> Result<(), String> {
    if args.is_empty() {
        return Err("用法: todo add <标题>".into());
    }
    let title = args.join(" ");                 // 切片拼接成新 String
    let mut todos = load()?;
    let next_id = todos.iter().map(|t| t.id).max().unwrap_or(0) + 1;
    let todo = Todo { id: next_id, title, done: false };
    println!("已添加: [{}] {}", todo.id, todo.title);
    todos.push(todo);                            // 所有权 move 进 Vec
    save(&todos)
}

fn cmd_list() -> Result<(), String> {
    let todos = load()?;
    if todos.is_empty() {
        println!("(空空如也)");
        return Ok(());
    }
    for t in &todos {                            // 借用遍历,不消耗 Vec
        let mark = if t.done { 'x' } else { ' ' };
        println!("[{}] {}. {}", mark, t.id, t.title);
    }
    Ok(())
}

fn cmd_done(args: &[String]) -> Result<(), String> {
    let id: u32 = args.get(0)
        .ok_or("用法: todo done <id>")?
        .parse()
        .map_err(|_| "id 必须是数字".to_string())?;
    let mut todos = load()?;
    let found = todos.iter_mut().find(|t| t.id == id);    // 可变借用
    match found {
        Some(t) => {
            t.done = true;
            println!("已完成: {}", t.title);
            save(&todos)
        }
        None => Err(format!("未找到 id={}", id)),
    }
}

fn main() {
    let args: Vec<String> = env::args().skip(1).collect();
    if args.is_empty() {
        eprintln!("用法: todo <add|list|done> [参数]");
        process::exit(1);
    }
    let cmd = &args[0];
    let rest = &args[1..];
    let result = match cmd.as_str() {
        "add"   => cmd_add(rest),
        "list"  => cmd_list(),
        "done"  => cmd_done(rest),
        _       => Err(format!("未知命令: {}", cmd)),
    };
    if let Err(e) = result {
        eprintln!("[错误] {}", e);
        process::exit(1);
    }
}

// === 用法示例(在终端依次运行)===
// $ cargo run -- add 买牛奶
// 已添加: [1] 买牛奶
// $ cargo run -- add 写季度报告
// 已添加: [2] 写季度报告
// $ cargo run -- list
// [ ] 1. 买牛奶
// [ ] 2. 写季度报告
// $ cargo run -- done 1
// 已完成: 买牛奶
// $ cargo run -- list
// [x] 1. 买牛奶
// [ ] 2. 写季度报告
//
// 文件 todos.json 内容(pretty 打印):
// [
//   {
//     "id": 1,
//     "title": "买牛奶",
//     "done": true
//   },
//   {
//     "id": 2,
//     "title": "写季度报告",
//     "done": false
//   }
// ]

要点讲解:

实战 2:多线程 HTTP 请求器(std::net + thread)

需求:用多线程并发请求一个 URL 共 N 次,记录每次响应时间,最后统计平均值/最大/最小。展示 Rust 的无畏并发——Arc + Mutex 跨线程共享数据,编译器在编译期就阻止数据竞争。

涉及知识:TcpStream 手写 HTTP 请求、std::thread::spawn 线程创建、Arc(原子引用计数)+ Mutex(互斥锁)共享可变状态、Instant 计时、map_err 错误转换、迭代器统计。纯标准库,无需第三方依赖。

// src/main.rs — 多线程 HTTP 请求器:并发请求 N 次,统计响应时间
// 无第三方依赖,纯标准库
// 运行:cargo run --release
use std::io::{Read, Write};
use std::net::TcpStream;
use std::sync::{Arc, Mutex};
use std::thread;
use std::time::Instant;

// 单次请求:返回耗时(毫秒)。map_err 把 io::Error 转成 String
fn request(host: &str, path: &str) -> Result<u128, String> {
    let start = Instant::now();
    let addr = format!("{}:80", host);
    let mut stream = TcpStream::connect(&addr)
        .map_err(|e| format!("连接失败: {}", e))?;
    // 手写一个最简 HTTP/1.1 请求(
 是 HTTP 协议规定)
    let req = format!(
        "GET {} HTTP/1.1\r\nHost: {}\r\nConnection: close\r\nUser-Agent: rust-bench/1.0\r\n\r\n",
        path, host
    );
    stream.write_all(req.as_bytes()).map_err(|e| format!("发送失败: {}", e))?;
    let mut buf = Vec::new();
    stream.read_to_end(&mut buf).map_err(|e| format!("读取失败: {}", e))?;
    Ok(start.elapsed().as_millis())
}

fn main() {
    let host = "example.com";
    let path = "/";
    let total: usize = 12;
    let workers = 4;                              // 并发线程数
    let per_worker = total / workers;

    // Arc(原子引用计数)+ Mutex(互斥锁):多线程安全共享 Vec
    let times: Arc<Mutex<Vec<u128>>> = Arc::new(Mutex::new(Vec::new()));
    let mut handles = vec![];

    for _ in 0..workers {
        let times = Arc::clone(&times);           // 引用计数 +1,所有权分享
        handles.push(thread::spawn(move || {      // move 把 times 所有权移入闭包
            for _ in 0..per_worker {
                match request(host, path) {
                    Ok(ms) => {
                        println!("[ok]  {} ms", ms);
                        times.lock().unwrap().push(ms);
                    }
                    Err(e) => println!("[err] {}", e),
                }
            }
        }));
    }

    for h in handles {
        h.join().unwrap();                        // 等所有线程结束
    }

    // 统计汇总:迭代器链一次性算出 sum/max/min
    let times = times.lock().unwrap();
    if times.is_empty() {
        println!("全部失败,无数据");
        return;
    }
    let sum: u128 = times.iter().sum();
    let max = *times.iter().max().unwrap();
    let min = *times.iter().min().unwrap();
    println!("--- 统计 ---");
    println!("成功 {}/{}", times.len(), total);
    println!("平均 {} ms | 最小 {} ms | 最大 {} ms", sum / times.len() as u128, min, max);
}

// 输出(实际数值取决于网络):
// [ok]  187 ms
// [ok]  142 ms
// [ok]  156 ms
// [ok]  198 ms
// ... (共 12 条)
// --- 统计 ---
// 成功 12/12
// 平均 165 ms | 最小 132 ms | 最大 241 ms

要点讲解:

实战 3:简易 grep(Iterator + 命令行参数)

需求:实现一个迷你 grep——在文件中搜索包含某关键词的所有行,输出 文件:行号:内容。展示 Rust 的迭代器组合和Trait 系统的优雅表达力。

涉及知识:std::fs::read_to_string、lines() 行迭代器、enumerate / filter / map / collect 迭代器链、字符串切片借用、env::args 命令行参数。纯标准库,无需依赖。

// src/main.rs — 简易 grep:在文件中搜索关键词,带行号输出
// 无第三方依赖
// 运行:cargo run -- <关键词> <文件>
use std::env;
use std::fs;

fn main() {
    let args: Vec<String> = env::args().collect();
    // args[0] 是程序名,args[1] 是关键词,args[2] 是文件路径
    if args.len() < 3 {
        eprintln!("用法: {} <关键词> <文件>", args[0]);
        std::process::exit(1);
    }
    let pattern = &args[1];
    let path = &args[2];

    let content = match fs::read_to_string(path) {
        Ok(c) => c,
        Err(e) => {
            eprintln!("无法读取 {}: {}", path, e);
            std::process::exit(1);
        }
    };

    // 迭代器链:lines → enumerate(加行号) → filter(匹配) → collect
    // 全部是惰性的(零成本抽象),只在 collect 时才真正遍历
    let hits: Vec<(usize, &str)> = content
        .lines()
        .enumerate()                              // (行索引, 行字符串)
        .filter(|(_, line)| line.contains(pattern))
        .map(|(i, line)| (i + 1, line))           // 行号从 1 开始
        .collect();                                // 收集成 Vec

    if hits.is_empty() {
        println!("未找到 \"{}\"", pattern);
        return;
    }

    for (lineno, line) in &hits {                  // 借用遍历
        println!("{}:{}: {}", path, lineno, line.trim());
    }
    println!("--- {} 处匹配 ---", hits.len());
}

// 假设文件 fruits.txt 内容:
//   apple
//   banana
//   apple pie
//   cherry
//   pineapple
//
// $ cargo run -- apple fruits.txt
// fruits.txt:1: apple
// fruits.txt:3: apple pie
// fruits.txt:5: pineapple
// --- 3 处匹配 ---
//
// $ cargo run -- xyz fruits.txt
// 未找到 "xyz"

要点讲解:

小结

三个项目分别对应 Rust 三大主力场景:应用开发(CLI 工具)、系统编程(并发与网络)、文本处理(迭代器组合)。它们都用到了前面学的基础——struct、enum、所有权、借用、Result、迭代器、Trait——只是组合起来解决真实问题。你会发现 Rust 代码"写的时候编译器帮你拦住一堆错,跑起来就基本没问题",这正是它"安全 + 性能兼得"的体感。建议你至少把其中一个改造成自己的版本(给 todo 加 delete 子命令、给 HTTP 请求器加 HTTPS 支持、给 grep 加正则),做过一遍比读十遍更扎实。

恭喜!到这里 Rust 系列就真正完结了。接下来挑一个方向深入——Web 后端(axum / actix-web)、异步运行时(tokio)、嵌入式(no_std)、WebAssembly(wasm-pack)、或者直接给开源项目(ripgrep、deno、tailwindcss 等 Rust 项目)提 PR——边做边学,进步最快。Rust 的学习曲线虽陡,但跨过去后,你写任何语言的代码都会更严谨。

← 返回 Rust 教程目录

上一篇 Rust 模块与 Crate

✈️💬