Rust 编译器总是在抱怨什么?

一个小小的命名警告,让我认识了 Rust 的 Lint 系统、编译器诊断,以及一件有些出乎意料的事:警告也能变成错误。

学 Rust 时,我写了一个再普通不过的函数:

rust 复制代码
fn interProduct(a: i32, b: i32) -> i32 {
    a * b
}
fn main() {
    println!("{}", interProduct(3, 4));
}

程序编译通过了,也能运行,输出的答案完全正确。

但 Rust 报了个警告。哦,原来只是警告啊,差点就忽略了。

等等,我得看看它到底抱怨什么:

typescript 复制代码
warning: function `interProduct` should have a snake case name
 --> src/main.rs:1:4
  |
1 | fn interProduct(a: i32, b: i32) -> i32 {
  |    ^^^^^^^^^^^^ help: convert the identifier to snake case: `inter_product`

我的第一反应是:代码合法,运行结果也正确,编译器为什么还要管我的函数叫什么?

后来才发现,顺着这个小警告,可以认识 Rust 中更多的内容:Lint、编译器诊断,以及"不合法的代码"和"只是可疑或不符合惯例的代码"之间的区别。

错误和警告有什么区别

大多数开发者都熟悉这个区别,不过在 Rust 里,我还是想再聊聊。

先从最简单的区别说起。看这段代码:

rust 复制代码
fn main() {
    let x: i32 = "hello";
}

这不是合法的 Rust 代码。

我们试图把字符串赋给一个 i32 类型的变量。编译器不能无视这个问题,然后生成正确的程序。

所以,这里会报错误(error),编译失败。

但下面这段代码就不一样了:

rust 复制代码
fn interProduct(a: i32, b: i32) -> i32 {
    a * b
}

把函数叫作 interProduct,本身并不会引起歧义或安全问题。Rust 完全能理解这个函数。

问题在于,Rust 的命名惯例要求函数名使用 snake_case

rust 复制代码
fn inter_product(a: i32, b: i32) -> i32 {
    a * b
}

因此,编译器没有拒绝这个程序,只是给出了警告。

我们可以先这样理解:

typescript 复制代码
Error
→ The compiler cannot accept the program.
→ Compilation fails.

Warning
→ The program is still valid.
→ Compilation can continue.
→ But the compiler found something worth our attention.

也就是说,错误会让编译失败;警告则允许编译继续,只是提醒我们,有些地方值得留意。

最后这一点就有意思了。

为什么要用 snake_case

编程中有几种常见的命名风格:

typescript 复制代码
snake_case
inter_product
camelCase
interProduct
PascalCase
InterProduct
SCREAMING_SNAKE_CASE
MAX_BUFFER_SIZE

Rust 对不同种类的标识符采用不同的命名惯例。

函数和变量通常使用 snake_case

rust 复制代码
fn calculate_total() {}
let user_name = "Harry";

结构体、枚举和 trait 的名称通常使用 PascalCase

rust 复制代码
struct UserAccount {}
enum Direction {
    Up,
    Down,
}
trait Printable {}

常量通常使用 SCREAMING_SNAKE_CASE

rust 复制代码
const MAX_BUFFER_SIZE: usize = 1024;

编译器不靠这些惯例也能理解程序。它们的作用是让代码保持一致。

当大多数 Rust 程序都遵循同样的惯例时,光看名字,我们就能大致知道眼前是什么

不同开发者写出来的代码,读起来也会更熟悉。

所以,Rust 可以建议我们这样改:

typescript 复制代码
interProduct
     ↓
inter_product

但这并不意味着原来的程序不合法。

警告有自己的名字

编译器并不是随便给出了一条提示。这个警告来自 Rust 的 Lint 系统

仔细看完整的诊断信息,会发现这一行:

typescript 复制代码
= note: `#[warn(non_snake_case)]` on by default

non_snake_case 就是一条 Lint 检查规则的名字。

Lint 检查的是那些可能不太妥当的代码写法,即使程序本身仍然合法。

比如:

rust 复制代码
fn main() {
    let x = 10;
}

这里的 x 从来没有被使用。Rust 可能会给出警告:

typescript 复制代码
warning: unused variable: `x`

同样,这个程序是合法的。编译器很清楚 x 是什么。

但它还是提醒我们看一眼,因为一个没用到的变量,可能意味着我们漏写了什么、犯了个错误,或者只是留下了多余的代码。

再让 Rust 多报几个警告

我修改了一下程序:

rust 复制代码
fn interProduct(a: i32, b: i32) -> i32 {
    a * b
}
fn main() {
    let unusedValue = 10;
    println!("{}", interProduct(3, 4));
}

然后运行:

sh 复制代码
cargo check

Rust 报了三个警告。

第一个是变量没有使用:

typescript 复制代码
warning: unused variable: `unusedValue`
help: if this is intentional, prefix it with an underscore:
`_unusedValue`

第二个是前面见过的函数命名警告:

typescript 复制代码
warning: function `interProduct` should have a snake case name
help: convert the identifier to snake case:
`inter_product`

第三个则是:

typescript 复制代码
warning: variable `unusedValue` should have a snake case name
help: convert the identifier to snake case:
`unused_value`

Rust 不只是告诉我们"这里不对"。

它会说明发现了什么,指出具体位置,标明对应的 Lint,而且通常还会建议我们怎么改。

对于没有使用的变量,它甚至考虑到了"我们就是故意不用它"的情况:

typescript 复制代码
help: if this is intentional, prefix it with an underscore

换句话说,诊断信息不只是在报告问题,也是在告诉我们:编译器为什么要提醒,以及我们可以怎么处理。

cargo check 是什么

这里我用的是:

sh 复制代码
cargo check

而不是:

sh 复制代码
cargo run

因为我并不需要实际运行程序。

cargo check 会检查项目及其依赖中的编译错误,并输出诊断信息,但会跳过最后的代码生成阶段。有些错误只有在代码生成时才会出现,所以通过检查并不保证完整构建一定成功。

开发时,如果我们只是想知道下面这件事,它通常就很方便:

代码还能编译吗?编译器又发现了什么问题?

对于小程序,这点区别可能不明显。项目大了以后,cargo check 就能成为日常开发中很方便的一环。

警告是可以配置的

Rust 的 Lint 系统还有个有意思的地方。

假设我就是想把这个函数叫作 interProduct,可以这样写:

rust 复制代码
#[allow(non_snake_case)]
fn interProduct(a: i32, b: i32) -> i32 {
    a * b
}

#[allow(...)] 会修改 Lint 的级别。

这里相当于在告诉 Rust:

我知道这不符合通常的命名惯例。这里就别提醒我了。

Rust 有多种 Lint 级别,包括:

typescript 复制代码
allow
warn
deny
forbid

现在先理解前三个就够用了。

  • allow 会抑制这条诊断信息。
  • warn 会把问题作为警告报告出来,但允许编译继续。
  • deny 则会把触发这条 Lint 的情况当作错误。

于是,我们可以做个有意思的实验。

把警告变成错误

我在程序文件开头加了一行:

rust 复制代码
#![deny(warnings)]


fn interProduct(a: i32, b: i32) -> i32 {
    a * b
}
fn main() {
    let unusedValue = 10;
    println!("{}", interProduct(3, 4));
}

然后运行:

sh 复制代码
cargo check

这次得到的是:

typescript 复制代码
error: unused variable: `unusedValue`

还有:

typescript 复制代码
error: function `interProduct` should have a snake case name

以及:

typescript 复制代码
error: variable `unusedValue` should have a snake case name

最后:

typescript 复制代码
error: could not compile `exercise`
due to 3 previous errors

代码本身并没有发生什么根本变化。之前触发三个警告的写法,现在却变成了三个错误。

原因就是这一行:

rust 复制代码
#![deny(warnings)]

编译器甚至解释了这一点:

typescript 复制代码
= note: `#[deny(unused_variables)]`
  implied by `#[deny(warnings)]`

原来的代码写法并没有突然变成语法错误。我们只是修改了项目的检查策略,决定更严格地对待这些 Lint。

#... 是什么

到这里,又出现了一种 Rust 语法:

rust 复制代码
#[allow(non_snake_case)]

这叫作属性(attribute)

属性可以给 Rust 程序的某个部分附加元数据或指令。你会经常看到它们:

rust 复制代码
#[test]
#[derive(Debug)]
#[allow(dead_code)]

这种形式:

rust 复制代码
#[...]

通常作用于紧跟在后面的项(item)。

比如:

rust 复制代码
#[allow(non_snake_case)]
fn interProduct() {}

但前面的例子用的是:

rust 复制代码
#![deny(warnings)]

注意这里的 !。这是内部属性(inner attribute),它作用于包含它的结构,而不只是后面的那一项。

把下面这行放在 main.rs 的开头:

rust 复制代码
#![deny(warnings)]

就相当于为整个 crate 设置了这项 Lint 策略。

目前不必记住所有属性,先能认出下面这两种语法就好:

rust 复制代码
#[...]
#![...]

Rust 会大量使用属性,向编译器提供额外的信息。

有警告,不一定就要改代码

这件事还有另一面。

Rust 给出警告,并不一定意味着:

你必须修改这段代码。

有时候,不遵循惯例也有合理的原因。

我们可能需要对接外部 API、处理生成的代码或 FFI 边界,也可能必须沿用已有的命名方式,或者受到其他无法控制的条件限制。

这就是 Rust 为什么会提供这样的工具:

rust 复制代码
#[allow(non_snake_case)]

而不是把每一条惯例都变成语言语法的一部分。

编译器不只是把关

编译器当然要拒绝违反语言规则的程序。但 Rust 的编译器诊断还会做更多事。

它经常会解释发生了什么,并引导我们找到可能的解决办法。

比如,这条警告:

typescript 复制代码
warning: unused variable: `unusedValue`

后面会跟着一条建议:

typescript 复制代码
help: if this is intentional,
prefix it with an underscore: `_unusedValue`

而这一条:

typescript 复制代码
warning: function `interProduct`
should have a snake case name

后面则会跟着:

typescript 复制代码
help: convert the identifier to snake case:
`inter_product`

这些警告和建议,都是有意义的!

《Rust 官方文档》强调,诊断信息应帮助开发者理解问题并找到解决办法。《Rust 程序设计语言》也提到,编译器错误可以引导开发者写出能正常工作的代码,而不只是阻止编译的障碍。

当然,这不意味着 Rust 编译器能保证代码写得"好"。它无法判断我们的架构是否合理、算法是否合适,也无法判断变量名是否真的传达了正确的含义。

不过,通过 Lint 和诊断信息,它能找出许多可疑、不一致或不符合 Rust 惯用写法的地方,提醒我们再看一眼。

别忘了 Clippy

Rust 还通过 Clippy 进一步扩展了这套检查。

你可以运行:

sh 复制代码
cargo clippy

Clippy 是 Rust 官方提供的一组额外的 Lint 检查。

rustc 本身已经能发现不少可疑的写法,而 Clippy 的检查范围更广,能帮助我们找出那些可能过于复杂、效率不高,或者不够符合 Rust 惯用写法的代码。

比如,我们写了:

rust 复制代码
if x == true {
    println!("yes");
}

Lint 工具就可以提醒我们,这里其实可以简化为:

rust 复制代码
if x {
    println!("yes");
}

这两种写法本身都是合法的。

检查的目的,是帮我们发现把 Rust 代码写得更清楚的机会。

一点想法

代码都能跑了,连函数叫什么都要管,Rust 确实有点"啰嗦"。不过,看完这些提示,我倒觉得这种啰嗦挺好。

我最喜欢 Rust 的一点是,它愿意把话说清楚。

哪里有问题,为什么提醒,建议怎么改。对正在学 Rust 的人来说,写代码时顺手读一读这些提示,就能慢慢熟悉这门语言的习惯。总比只丢给你一句"错了",然后让你自己去猜,要好得多。

下次看到警告,先别急着忽略。点进去看一眼,也许只是改个名字,也许真能发现自己漏掉的东西。有人愿意在代码运行之前多提醒几句,我觉得是件好事。

相关推荐
Ramble_Naylor17 小时前
错误是一种值:Result、? 与 panic!
rust
柯南466818 小时前
【AI开发之Rust】第 10 课:Cargo 工程化 —— 模块、依赖与测试(基础篇收尾)
rust·编程语言
qq_4523962318 小时前
第九篇:《并发编程:线程、Channel 与共享状态》
rust
Source.Liu19 小时前
【A11】 labelprinter(Tauri 版)新建步骤
windows·rust
孙启超20 小时前
【AI开发之Rust】第 8 课:泛型、trait 与 trait 对象
开发语言·后端·rust
蓝宝石的傻话20 小时前
MiBee Eye Notebook:把闲置笔记本变成一台带麦克风的网络摄像头
数码相机·rust
小灰灰搞电子21 小时前
Rust Once 、OnceLock、LazyLock 一次性初始化详解
开发语言·后端·rust
qq_452396231 天前
第八篇:《智能指针与内部可变性:Rust 的进阶内存管理》
rust
Amos_Web1 天前
Rspack 源码解析(七):Module、Chunk 与加载树优化
前端·rust·源码阅读