Rust 开发模式
用于构建安全、高性能且易于维护的应用程序的惯用 Rust 模式和最佳实践。
何时使用
- 编写新的 Rust 代码
- 审查 Rust 代码
- 重构现有的 Rust 代码
- 设计 crate 结构和模块布局
工作原理
本技能在六个关键领域贯彻惯用的 Rust 约定:所有权与借用(在编译时防止数据竞争)、Result/? 错误传播(库使用 thiserror,应用使用 anyhow)、枚举与穷尽模式匹配(使非法状态不可表示)、trait 与泛型(零成本抽象)、安全的并发(通过 Arc<Mutex<T>>、通道和 async/await),以及按领域组织的最小 pub 接口。
核心原则
1. 所有权与借用
Rust 的所有权系统在编译时防止数据竞争和内存错误。
// 好:不需要所有权时传递引用
fn process(data: &[u8]) -> usize {
data.len()
}
// 好:仅在需要存储或消费时获取所有权
fn store(data: Vec<u8>) -> Record {
Record { payload: data }
}
// 坏:为避免借用检查器而不必要地克隆
fn process_bad(data: &Vec<u8>) -> usize {
let cloned = data.clone(); // 浪费——直接借用即可
cloned.len()
}
使用 Cow 实现灵活的所有权
use std::borrow::Cow;
fn normalize(input: &str) -> Cow<'_, str> {
if input.contains(' ') {
Cow::Owned(input.replace(' ', "_"))
} else {
Cow::Borrowed(input) // 无需修改时零成本
}
}
错误处理
使用 Result 和 ? —— 生产中绝不使用 unwrap()
// 好:传播错误并附带上下文
use anyhow::{Context, Result};
fn load_config(path: &str) -> Result<Config> {
let content = std::fs::read_to_string(path)
.with_context(|| format!("从 {path} 读取配置失败"))?;
let config: Config = toml::from_str(&content)
.with_context(|| format!("从 {path} 解析配置失败"))?;
Ok(config)
}
// 坏:错误时直接 panic
fn load_config_bad(path: &str) -> Config {
let content = std::fs::read_to_string(path).unwrap(); // 会 panic!
toml::from_str(&content).unwrap()
}
库错误用 thiserror,应用错误用 anyhow
// 库代码:结构化的、类型化的错误
use thiserror::Error;
#[derive(Debug, Error)]
pub enum StorageError {
#[error("未找到记录: {id}")]
NotFound { id: String },
#[error("连接失败")]
Connection(#[from] std::io::Error),
#[error("无效数据: {0}")]
InvalidData(String),
}
// 应用代码:灵活的错误处理
use anyhow::{bail, Result};
fn run() -> Result<()> {
let config = load_config("app.toml")?;
if config.workers == 0 {
bail!("工作线程数必须大于 0");
}
Ok(())
}
使用 Option 的组合器而非嵌套匹配
// 好:组合器链
fn find_user_email(users: &[User], id: u64) -> Option<String> {
users.iter()
.find(|u| u.id == id)
.map(|u| u.email.clone())
}
// 坏:深度嵌套的匹配
fn find_user_email_bad(users: &[User], id: u64) -> Option<String> {
match users.iter().find(|u| u.id == id) {
Some(user) => match &user.email {
email => Some(email.clone()),
},
None => None,
}
}
枚举与模式匹配
将状态建模为枚举
// 好:不可能的状态是不可表示的
enum ConnectionState {
Disconnected,
Connecting { attempt: u32 },
Connected { session_id: String },
Failed { reason: String, retries: u32 },
}
fn handle(state: &ConnectionState) {
match state {
ConnectionState::Disconnected => connect(),
ConnectionState::Connecting { attempt } if *attempt > 3 => abort(),
ConnectionState::Connecting { .. } => wait(),
ConnectionState::Connected { session_id } => use_session(session_id),
ConnectionState::Failed { retries, .. } if *retries < 5 => retry(),
ConnectionState::Failed { reason, .. } => log_failure(reason),
}
}
穷尽匹配 —— 业务逻辑中不要使用通配符
// 好:显式处理每个变体
match command {
Command::Start => start_service(),
Command::Stop => stop_service(),
Command::Restart => restart_service(),
// 添加新变体时,必须在此处处理
}
// 坏:通配符隐藏了新变体
match command {
Command::Start => start_service(),
_ => {} // 静默忽略了 Stop、Restart 以及未来的变体
}
Traits 与泛型
接受泛型,返回具体类型
// 好:泛型输入,具体输出
fn read_all(reader: &mut impl Read) -> std::io::Result<Vec<u8>> {
let mut buf = Vec::new();
reader.read_to_end(&mut buf)?;
Ok(buf)
}
// 好:为多个约束使用 trait bound
fn process<T: Display + Send + 'static>(item: T) -> String {
format!("已处理: {item}")
}
为动态分发使用 Trait 对象
// 当你需要异构集合或插件系统时使用
trait Handler: Send + Sync {
fn handle(&self, request: &Request) -> Response;
}
struct Router {
handlers: Vec<Box<dyn Handler>>,
}
// 当你需要性能时使用泛型(单态化)
fn fast_process<H: Handler>(handler: &H, request: &Request) -> Response {
handler.handle(request)
}
新类型模式实现类型安全
// 好:不同的类型防止参数混淆
struct UserId(u64);
struct OrderId(u64);
fn get_order(user: UserId, order: OrderId) -> Result<Order> {
// 不会意外交换用户和订单 ID
todo!()
}
// 坏:容易交换参数
fn get_order_bad(user_id: u64, order_id: u64) -> Result<Order> {
todo!()
}
结构体与数据建模
复杂构造的构建器模式
struct ServerConfig {
host: String,
port: u16,
max_connections: usize,
}
impl ServerConfig {
fn builder(host: impl Into<String>, port: u16) -> ServerConfigBuilder {
ServerConfigBuilder { host: host.into(), port, max_connections: 100 }
}
}
struct ServerConfigBuilder { host: String, port: u16, max_connections: usize }
impl ServerConfigBuilder {
fn max_connections(mut self, n: usize) -> Self { self.max_connections = n; self }
fn build(self) -> ServerConfig {
ServerConfig { host: self.host, port: self.port, max_connections: self.max_connections }
}
}
// 用法:ServerConfig::builder("localhost", 8080).max_connections(200).build()
迭代器与闭包
优先使用迭代器链而非手动循环
// 好:声明式、惰性、可组合
let active_emails: Vec<String> = users.iter()
.filter(|u| u.is_active)
.map(|u| u.email.clone())
.collect();
// 坏:命令式累积
let mut active_emails = Vec::new();
for user in &users {
if user.is_active {
active_emails.push(user.email.clone());
}
}
使用带类型注解的 collect()
// 收集到不同类型中
let names: Vec<_> = items.iter().map(|i| &i.name).collect();
let lookup: HashMap<_, _> = items.iter().map(|i| (i.id, i)).collect();
let combined: String = parts.iter().copied().collect();
// 收集 Results —— 在第一个错误时短路
let parsed: Result<Vec<i32>, _> = strings.iter().map(|s| s.parse()).collect();
并发
共享可变状态使用 Arc<Mutex<T>>
use std::sync::{Arc, Mutex};
let counter = Arc::new(Mutex::new(0));
let handles: Vec<_> = (0..10).map(|_| {
let counter = Arc::clone(&counter);
std::thread::spawn(move || {
let mut num = counter.lock().expect("mutex 被污染");
*num += 1;
})
}).collect();
for handle in handles {
handle.join().expect("工作线程 panic");
}
消息传递使用通道
use std::sync::mpsc;
let (tx, rx) = mpsc::sync_channel(16); // 有界通道,带背压
for i in 0..5 {
let tx = tx.clone();
std::thread::spawn(move || {
tx.send(format!("消息 {i}")).expect("接收端已断开");
});
}
drop(tx); // 关闭发送端,以便 rx 迭代器终止
for msg in rx {
println!("{msg}");
}
使用 Tokio 实现异步
use tokio::time::Duration;
async fn fetch_with_timeout(url: &str) -> Result<String> {
let response = tokio::time::timeout(
Duration::from_secs(5),
reqwest::get(url),
)
.await
.context("请求超时")?
.context("请求失败")?;
response.text().await.context("读取响应体失败")
}
// 生成并发任务
async fn fetch_all(urls: Vec<String>) -> Vec<Result<String>> {
let handles: Vec<_> = urls.into_iter()
.map(|url| tokio::spawn(async move {
fetch_with_timeout(&url).await
}))
.collect();
let mut results = Vec::with_capacity(handles.len());
for handle in handles {
results.push(handle.await.unwrap_or_else(|e| panic!("生成的任务 panic: {e}")));
}
results
}
不安全代码
何时可以接受不安全代码
// 可接受:FFI 边界,记录有不变式(Rust 2024+)
/// # Safety
/// `ptr` 必须是已初始化 `Widget` 的有效对齐指针。
unsafe fn widget_from_raw<'a>(ptr: *const Widget) -> &'a Widget {
// SAFETY:调用者保证 ptr 有效且对齐
unsafe { &*ptr }
}
// 可接受:性能关键路径,附有正确性证明
// SAFETY:由于循环边界,index 始终 < len
unsafe { slice.get_unchecked(index) }
何时 unsafe 是 NOT 可接受的
// 坏:使用 unsafe 绕过借用检查器
// 坏:为了方便使用 unsafe
// 坏:使用 unsafe 但未添加 Safety 注释
// 坏:在不相关类型之间进行 transmute
模块系统与 Crate 结构
按领域组织,而非按类型
my_app/
├── src/
│ ├── main.rs
│ ├── lib.rs
│ ├── auth/ # 领域模块
│ │ ├── mod.rs
│ │ ├── token.rs
│ │ └── middleware.rs
│ ├── orders/ # 领域模块
│ │ ├── mod.rs
│ │ ├── model.rs
│ │ └── service.rs
│ └── db/ # 基础设施
│ ├── mod.rs
│ └── pool.rs
├── tests/ # 集成测试
├── benches/ # 基准测试
└── Cargo.toml
可见性 —— 最小化暴露
// 好:使用 pub(crate) 进行内部共享
pub(crate) fn validate_input(input: &str) -> bool {
!input.is_empty()
}
// 好:从 lib.rs 重新导出公共 API
pub mod auth;
pub use auth::AuthMiddleware;
// 坏:将所有内容设为 pub
pub fn internal_helper() {} // 应该是 pub(crate) 或私有
工具集成
基本命令
# 构建和检查
cargo build
cargo check # 快速类型检查,不生成代码
cargo clippy # lint 和建议
cargo fmt # 格式化代码
# 测试
cargo test
cargo test -- --nocapture # 显示 println 输出
cargo test --lib # 仅单元测试
cargo test --test integration # 仅集成测试
# 依赖
cargo audit # 安全审计
cargo tree # 依赖树
cargo update # 更新依赖
# 性能
cargo bench # 运行基准测试
快速参考:Rust 惯用法
| 惯用法 | 描述 |
|---|---|
| 借用,不要克隆 | 除非需要所有权,否则传递 &T 而不是克隆 |
| 使非法状态不可表示 | 使用枚举建模仅有效状态 |
? 优先于 unwrap() | 传播错误,在库/生产代码中绝不 panic |
| 解析,不要验证 | 在边界将非结构化数据转换为类型化结构体 |
| 新类型实现类型安全 | 用新类型包装基本类型,防止参数交换 |
| 优先使用迭代器而非循环 | 声明式链更清晰,通常也更快 |
在 Result 上使用 #[must_use] | 确保调用者处理返回值 |
Cow 实现灵活所有权 | 在借用足够时避免分配 |
| 穷尽匹配 | 在关键业务枚举中不使用通配符 _ |
最小的 pub 接口 | 内部 API 使用 pub(crate) |
应避免的反模式
// 坏:生产代码中使用 .unwrap()
let value = map.get("key").unwrap();
// 坏:为满足借用检查器而 .clone(),但不理解原因
let data = expensive_data.clone();
process(&original, &data);
// 坏:在 &str 足够时使用 String
fn greet(name: String) { /* 应该是 &str */ }
// 坏:在库中使用 Box<dyn Error>(应使用 thiserror)
fn parse(input: &str) -> Result<Data, Box<dyn std::error::Error>> { todo!() }
// 坏:忽略 must_use 警告
let _ = validate(input); // 静默丢弃了一个 Result
// 坏:在异步上下文中阻塞
async fn bad_async() {
std::thread::sleep(Duration::from_secs(1)); // 阻塞执行器!
// 使用:tokio::time::sleep(Duration::from_secs(1)).await;
}
请记住:如果编译通过,那么可能是正确的——但前提是你避免使用 unwrap(),尽可能减少 unsafe,并让类型系统为你工作。