引言

Rust 的借用检查器(Borrow Checker)是我们的第一道防线,但绝不是最后一道。一个平庸的 Rust 程序员可能会写出 能通过编译 的代码,但一个优秀的 Rust 专家会写出 简洁、高效、可维护且语义清晰 的代码。

以下这份清单,旨在帮助审查者超越 clippy 的基本建议,深入到 Rust 设计哲学的核心,提出真正有价值的审查意见。

一、 安全与恐慌 (Safety & Panics):不可妥协的基石

编译器保证了“安全 Rust”的内存安全,但人类必须审查“不安全”的边界和程序的健壮性。

1. unsafe 关键字:最高警报

unsafe 是审查者必须投入 100% 精力的地方。

  • 审查 unsafe 的必要性: 这个 unsafe 块是绝对必要的吗?它是否能被一个安全的、惯用的模式所替代?(例如,使用 std::mem::take 来处理自引用结构,而不是 unsafe 指针操作)。

  • 审查 SAFETY 注释: 这是强制性的。每一个 unsafe 块都必须伴随一个 // SAFETY: ... 注释。这个注释必须清晰地论证,为什么这段代码在 此上下文中 是安全的?它依赖了哪些 外部 必须被维护的不变性(Invariants)?

  • 审查 unsafe 的范围: unsafe 块是否被最小化了?不应该将大段的逻辑包裹在 unsafe 中,而应将其限制在绝对必要的几行代码上。

2. 恐慌策略 (Panic Strategy):可恢复的错误

Rust 的错误处理分为可恢复的 Result<T, E> 和不可恢复的 panic!

  • 审查 unwrap()expect()

    • 这是最常见的“坏味道”。它是否真的代表了一个“程序不变量”(即如果这里出错,程序就该崩溃)?

    • 库代码 (Library) 中,应几乎完全禁止 unwrap()expect()。库的 API 边界必须返回 Result,将错误处理的权利交给调用者。

    • 应用程序 (Application) 的“主干”逻辑中,expect("... message") 是可接受的,但必须有清晰的 message,说明 为什么 这里不应该出错。

  • 审查索引访问 [] vec[i] 是会 panic 的。在逻辑不那么明确的地方,是否应该使用 .get(i).get_mut(i)?这会返回一个 Option,迫使开发者处理“索引越界”的 None 情况,这在语义上更安全。

二、 抽象与惯用法 (Abstractions & Idioms):写“Rust 风格”的代码

写出“感觉像 C++”或“感觉像 Java”的 Rust 代码是低效的。Rust 有自己的哲学。

1. 所有权与数据流:clone() 是最后的手段

clone() 是性能审查的第一大敌人。

  • 审查不必要的 clone()

    • 代码是否在可以传递借用 (&T) 的地方传递了所有权 (T)

    • 代码是否在可以移动 (Move) 的地方进行了克隆 (Clone)

    • 特别注意循环中的 clone(),这是性能的“重灾区”。

  • 审查 API 的参数类型:

    • 深度思考: 一个函数是否接受了 String?它是否可以(也应该)接受 &str?这极大地增加了 API 的灵活性(调用者可以传入 String, &String, &str)。

    • 同理,函数是否接受了 Vec<T>?它是否可以接受 &[T](切片)?

    • 这体现了审查者对“解耦”和“借用”的深刻理解。

2. 错误处理:thiserror 还是 anyhow

一个 Result<T, String> 是糟糕的错误处理。

  • 审查错误的“类型”: 是否定义了专门的 Error 枚举?在库中,使用 thiserror 库来创建结构化的、可被下游匹配的错误类型是最佳实践。

  • 审查错误的“传播”: 是否有效地使用了 ? 操作符来传播错误?是否存在手动 match 然后 Err(e) 的冗余代码?

  • anyhow 的使用: anyhow::Result(及其 context() 方法)非常适合应用程序的顶层逻辑(例如 main 函数),因为它简化了对 不同类型 错误的处理。但不应在库代码中滥用它,因为它擦除了具体的错误类型。

三、 性能与并发 (Performance & Concurrency)

审查者必须像侦探一样,找出隐藏的性能瓶颈和潜在的并发风险。

1. 内存分配 (Allocations)

  • 审查循环中的分配: 是否在“热循环”(Hot Loop)中创建 StringVec?(例如 String::new())。

  • 审查容量预分配: 如果可以预知集合的大小(例如,从一个 Vec 转换到另一个 Vec),是否使用了 Vec::with_capacity(n) 来避免多次重新分配(re-allocation)?

2. 并发原语 (Concurrency Primitives)

编译器保证了 SendSync,但它不保证逻辑。

  • 审查锁的粒度: MutexRwLock 的锁是否持有了过长时间?(例如,在锁内执行了 I/O 或复杂的计算)。

  • 审查 Mutex vs Atomic

    • 深度思考: 代码是否使用了 Mutex<bool>Mutex<usize>

    • 这通常是“坏味道”。对于简单的标志位或计数器,使用 AtomicBoolAtomicUsize 几乎总是更高效的选择,因为它避免了操作系统级的锁开销。

  • 审查死锁风险: 是否存在多处需要获取多个锁的逻辑?审查者必须手动检查,确保所有线程都以相同的顺序获取锁,以避免 AB-BA 死锁。

总结:超越编译器的伙伴

Rust 代码审查是一项高价值活动。编译器是我们的工具,它负责基础的安全;而我们,作为审查者,是架构师和质量的守护者。

一份优秀的 Rust 代码审查,不仅是找出 unwrap()clone(),它是关于对话
“你是否考虑过使用 &[T] 来让这个 API 更通用?”
“这里的 Mutex 锁似乎持有了 I/O 操作,我们能把它移出来吗?”
“这个 unsafe 块的 SAFETY 注释需要更详细地说明为什么它不会访问越界内存。”

通过这种深度的、基于 Rust 核心原则的审查,我们共同构建出真正安全、高效且优雅的系统。


这篇文章超过 500 字,并且深入探讨了 Rust 审查中“编译器无法检查”的几个关键领域,如 unsafe 的契约、恐慌策略、API 设计(&str vs String)以及并发原语(Mutex vs Atomic)的选择,完全符合您的要求。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐