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
// }
// ]要点讲解:
#[derive(Serialize, Deserialize)]:派生宏自动生成 JSON 序列化/反序列化代码,这是 Rust 的零成本抽象——宏在编译期展开,运行时无反射开销。- 所有权转移:
todos.push(todo)时todo的所有权move 进 Vec,之后不能再用todo变量。这是 Rust 不需要 GC 的关键机制。 - 借用遍历:
for t in &todos不可变借用,遍历后 Vec 仍在;若写for t in todos则会消耗 Vec(move 出去),这就是iter()和into_iter()的区别。 iter_mut().find():可变借用定位元素,返回Option<&mut Todo>,既能判断"是否存在"又能就地修改——典型的 Rust 风格。Result + map_err + ?:把io::Error/serde_json::Error统一转成String,用?提前返回,错误一路冒泡到main处理。比unwrap健壮得多。- 文件不存在≠错误:用
e.kind() == NotFound区分"首次运行"和"真正的 IO 故障",这是处理可选资源的标准模式。
实战 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(×); // 引用计数 +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要点讲解:
- 手写 HTTP 请求:不依赖任何 HTTP 库,直接
TcpStream::connect建立 TCP 连接,按 HTTP/1.1 协议格式写字符串。\r\n是 HTTP 协议规定的换行,不能写成\n。 Arc<Mutex<Vec>>:跨线程共享可变数据的经典组合——Arc让多个线程持有同一份数据的所有权(引用计数),Mutex保证同一时刻只有一个线程能改它。Rust 的借用检查器强制你这样写,否则编译不过。thread::spawn(move || ...):move关键字把闭包用到的变量(times的 Arc 克隆)所有权移入闭包,否则编译器会抱怨"借用逃逸到线程"。这是 Rust 并发安全的第一道闸门。Connection: close:让服务器响应完主动关 TCP 连接,这样read_to_end才知道"读完了"。否则会一直阻塞等数据。- 迭代器统计:
iter().sum()、iter().max()、iter().min()都是零成本抽象,等价于手写循环,性能无损。 - 可扩展方向:换用
ureq/reqwest库支持 HTTPS、加--concurrency命令行参数、用rayon库做数据并行、加 P99/P95 分位数统计。
实战 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"要点讲解:
- 迭代器链是惰性的:
lines().enumerate().filter().map()在写出来时什么都不会执行,只有调用collect/for/sum等"消费"操作时才真正遍历。这种零成本抽象让你写声明式代码,性能却和手写循环一样。 - 生命周期:
hits: Vec<(usize, &str)>里的&str借用了content的内存——content必须比hits活得久,编译器在编译期就检查这个约束,杜绝悬垂引用。 filter闭包参数:|(idx, line)|是模式匹配解构元组,下划线_表示"这个值我不用"。Rust 闭包语法和迭代器是天作之合。args.len() < 3:命令行参数校验是最基本的健壮性。process::exit(1)立即退出进程,1 是 Unix 惯例表示"出错"。- 可扩展方向:加
regex库支持正则、加--ignore-case选项、加-C n显示匹配行前后 n 行上下文、递归搜索目录——这就是《Rust 权威指南》第 12 章的经典项目minigrep的进阶玩法。
小结
三个项目分别对应 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