Alby's blog

世上没有巧合,只有巧合的假象。

0%

Rust 坏味道与改进指南

前言

Rust 编译器帮助我们避免了许多常见的内存安全和并发错误,但依然存在一些“坏味道”。这些坏味道不会直接导致编译错误,却可能意味着代码在可维护性、性能或抽象设计上存在问题。本文参考《重构》的风格,整理了常见的 Rust 坏味道及改进方式。

内容

1. 到处 .clone()

坏味道示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
fn main() {
let s = String::from("hello");

// 既想借用 s,又想把 s move 给另一个函数
print_ref(&s);
consume(s.clone()); // 这里乱 clone 以逃避所有权
print_ref(&s);
}

fn print_ref(s: &str) {
println!("ref: {}", s);
}

fn consume(s: String) {
println!("consumed: {}", s);
}

改进方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
fn main() {
let s = String::from("hello");

print_ref(&s);
consume(&s); // 传引用就行
print_ref(&s);
}

fn print_ref(s: &str) {
println!("ref: {}", s);
}

fn consume(s: &str) {
println!("consumed: {}", s);
}

要点: 优先借用 &T,仅在必要时使用 clone。过度拷贝会导致性能问题。


2. 随手 .unwrap()

坏味道示例:

1
let v = s.parse::<i32>().unwrap();

改进方式:

1
let v: i32 = s.parse()?;

要点: 使用 ? 传播错误,返回 Result,避免随时 panic。


3. API 参数总用 String

坏味道示例:

1
2
3
fn greet(name: String) {
println!("Hello, {}", name);
}

改进方式:

1
2
3
fn greet(name: &str) {
println!("Hello, {}", name);
}

要点: 参数尽量使用 &str,调用方无需额外分配内存。


4. 错误类型用 String

坏味道示例:

1
2
3
fn parse_age(s: &str) -> Result<u32, String> {
s.parse::<u32>().map_err(|_| "Invalid number".to_string())
}

改进方式:

1
2
3
4
5
6
7
8
#[derive(Debug)]
enum ParseError {
InvalidNumber,
}

fn parse_age(s: &str) -> Result<u32, ParseError> {
s.parse::<u32>().map_err(|_| ParseError::InvalidNumber)
}

要点: 定义枚举错误类型,或使用 thiserror/anyhow


5. 滥用 Rc<RefCell<T>>

坏味道示例:

1
2
3
4
5
6
7
8
9
10
11
12
use std::rc::Rc;
use std::cell::RefCell;

struct Counter {
value: Rc<RefCell<i32>>,
}

impl Counter {
fn inc(&self) {
*self.value.borrow_mut() += 1;
}
}

改进方式:

1
2
3
4
5
6
7
8
9
struct Counter {
value: i32,
}

impl Counter {
fn inc(&mut self) {
self.value += 1;
}
}

要点: 优先使用借用和 &mut,必要时再考虑智能指针。


6. 深层 match 嵌套

坏味道示例:

1
2
3
4
5
6
7
match option {
Some(v) => match v {
Ok(x) => println!("{}", x),
Err(_) => println!("error"),
},
None => println!("none"),
}

改进方式:

1
2
3
if let Some(Ok(x)) = option {
println!("{}", x);
}

或者使用 map, and_then
要点: 利用模式匹配简化控制流。


7. 到处 mut

坏味道示例:

1
2
3
4
let mut x = 0;
for i in 0..10 {
x += i;
}

改进方式:

1
let x: i32 = (0..10).sum();

要点: 优先函数式写法,减少可变状态。


8. 无意义的 trait 实现

坏味道示例:

1
2
3
4
5
impl Default for Config {
fn default() -> Self {
Config {}
}
}

改进方式: 仅在 Default 有合理含义时实现。
要点: 避免“为了编译过”而写无语义代码。


9. 模块全 pub

坏味道示例:

1
2
3
4
pub struct Foo {
pub a: i32,
pub b: i32,
}

改进方式:

1
2
3
4
5
6
7
8
9
pub struct Foo {
a: i32,
b: i32,
}

impl Foo {
pub fn new(a: i32, b: i32) -> Self { Self { a, b } }
pub fn a(&self) -> i32 { self.a }
}

要点: 隐藏实现细节,提供受控接口。


10. 在热路径频繁分配

坏味道示例:

1
2
3
4
for _ in 0..1000 {
let v = Vec::new();
// ...
}

改进方式:

1
2
3
4
5
let mut v = Vec::with_capacity(1000);
for _ in 0..1000 {
v.clear();
// ...
}

要点: 预分配或复用 buffer。


11. 随意 unsafe

坏味道示例:

1
unsafe { *ptr = 5; }

改进方式:

  • 封装在安全 API 内
  • 写明 invariants(不变量)

要点: unsafe 应该是小心隔离的边角代码,不是随处可见的习惯。


12. 缺乏测试与 bench

坏味道示例: 项目中无单元测试。
改进方式: 使用 Rust 内置测试框架:

1
2
3
4
5
6
7
8
9
#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_sum() {
assert_eq!((0..10).sum::<i32>(), 45);
}
}

要点: 测试和 bench 是防止坏味道扩散的护栏。


总结

Rust 的坏味道主要集中在 所有权处理、错误管理、模块设计 上。如果代码中到处充斥 .clone().unwrap()Rc<RefCell>Result<T, String>,或 pub struct 全开放,那就是需要重构的信号。本手册希望成为 Rust 程序员日常自检与改进的参考。