
一个小小的命名警告,让我认识了 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 的人来说,写代码时顺手读一读这些提示,就能慢慢熟悉这门语言的习惯。总比只丢给你一句"错了",然后让你自己去猜,要好得多。
下次看到警告,先别急着忽略。点进去看一眼,也许只是改个名字,也许真能发现自己漏掉的东西。有人愿意在代码运行之前多提醒几句,我觉得是件好事。