坦白说,我第一次尝试 Rust 的时候,真想把笔记本电脑扔出窗外。借用检查器一直对我“大喊大叫”。那些在 Python 或 JavaScript 中本可以正常运行的代码,在这里就是无法编译通过。我花了两个小时调试一个问题,结果发现是生命周期注解的问题。

但有件事没人告诉你:那种挫败感?其实是这门语言在保护你,让你免于犯错。
第一个星期简直是地狱
每个学习 Rust 的人都会经历这个阶段。你写下一些看起来完全合理的代码:
fn main() {
let data = vec![1, 2, 3, 4, 5];
let first = &data[0];
data.push(6);
println!("First element: {}", first);
}
然后编译器只会告诉你“不行”。当你持有一个指向 data 的引用时,你不能修改它。在大多数语言中,这段代码都能编译通过。甚至可能在大部分时间里都能正常工作。直到有一天它突然罢工,而你不得不在凌晨两点焦头烂额地调试一个生产环境的事故。
Rust 编译器基本上就是那个烦人的朋友,总是不停地指出所有可能出错的地方。但不同的是,这个朋友永远是对的。
到底是什么让 Rust 这么难?
不是语法。它的语法其实相当简洁。真正让所有人头疼的是这三个概念:
- 所有权 (Ownership) — 每个值都有一个所有者。当所有者离开作用域时,这个值就会被清理掉。
- 借用 (Borrowing) — 你可以把数据的引用“借”出去,但必须遵守规则。要么只有一个可变引用,要么有多个不可变引用,但绝不能同时存在。
- 生命周期 (Lifetimes) — 有时候你需要告诉编译器,这些引用的存活时间是多久。
下面这个真实的例子能让你豁然开朗:
struct User {
name: String,
email: String,
}
fn main() {
let user = User {
name: String::from("Alice"),
email: String::from("alice@example.com"),
};
process_user(user);
// user is gone now, ownership moved
// println!("{}", user.name); // This won't compile
}
fn process_user(u: User) {
println!("Processing {}", u.name);
}
变量 user 被移动到了 process_user 中。在那之后,它就消失了。你再也不能使用它了。一开始这感觉很奇怪,但这意味着 Rust 能精确地知道何时释放内存。完全不需要垃圾回收器。
某个时刻,你突然就顿悟了
在和编译器“搏斗”了几周后,一件奇妙的事情发生了。你开始用不同的方式思考你的代码。你甚至在下笔之前就能预见到潜在的 bug。
我记得我顿悟的那个瞬间。当时我正在编写一个处理用户会话的 Web 服务。在我之前的 Go 代码库里,我们曾遇到一个棘手的 bug:两个 goroutine 有时会同时修改同一个会话 map。这是一个竞争条件。它很少发生,但一旦出现,调试起来就是一场噩梦。
而在 Rust 中,我根本就写不出那样的 bug。编译器直接阻止了我:
use std::collections::HashMap;
struct SessionStore {
sessions: HashMap<String, String>,
}
impl SessionStore {
fn new() -> Self {
SessionStore {
sessions: HashMap::new(),
}
}
fn add_session(&mut self, id: String, data: String) {
self.sessions.insert(id, data);
}
fn get_session(&self, id: &str) -> Option<&String> {
self.sessions.get(id)
}
}
注意到 add_session 中的 &mut self 和 get_session 中的 &self 了吗?这正是 Rust 在表达“当有人正在写入时,你不能读取”的方式。类型系统强制保证了线程安全。没有运行时检查,也没有那些你可能会忘记获取的锁。
生产环境才是 Rust 大放异彩的地方
关于在生产环境中使用 Rust,有件事没人告诉你:你部署了它,然后……什么事都没发生。我这么说,绝对是最好的褒义。
没有内存泄漏慢慢吞噬你的 RAM。没有空指针异常导致的随机崩溃。也没有那些只在高负载下才会出现的“在我机器上能跑”的 bug。
我们将一个服务从 Node.js 迁移到了 Rust。Node 版本大约使用 400MB 内存,偶尔会飙升到 1GB。而 Rust 版本呢?50MB。非常稳定。没有峰值。没有垃圾回收导致的停顿。
// A simple HTTP handler that just works
use actix_web::{web, App, HttpResponse, HttpServer};
async fn health_check() -> HttpResponse {
HttpResponse::Ok().body("OK")
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
HttpServer::new(|| {
App::new()
.route("/health", web::get().to(health_check))
})
.bind("127.0.0.1:8080")?
.run()
.await
}
这段代码每秒能处理数千个请求。无需调优。异步运行时是内置的。错误处理是显式的。如果某个地方可能会失败,类型系统会强制你处理它。
错误处理的故事
说到错误处理,这正是 Rust 真正征服我的地方。没有异常。没有你可能会忘记的 try-catch 代码块。只有一个 Result 类型:
use std::fs::File;
use std::io::Read;
fn read_config(path: &str) -> Result<String, std::io::Error> {
let mut file = File::open(path)?;
let mut contents = String::new();
file.read_to_string(&mut contents)?;
Ok(contents)
}
fn main() {
match read_config("config.txt") {
Ok(config) => println!("Config: {}", config),
Err(e) => println!("Failed to read config: {}", e),
}
}
那个 ? 操作符做了很多工作。它的意思是“如果这步失败了,就返回错误”。你无法忽略它,也无法忘记处理它。因为编译器不允许你这么做。
真实的学习曲线
Rust 真实的学习曲线是这样的:
Frustration Level
^
| *
| *
| *
| * * * * * *
| *
| *
|*___________________*_________> Time
Week 1 Week 3 Week 6
- 第 1-2 周:万事开头难。编译器很“刻薄”。你开始怀疑人生。
- 第 3-4 周:有些东西开始变得清晰了。你仍然在和借用检查器“搏斗”,但频率降低了。
- 第 5-6 周:豁然开朗。你开始能写出一次性编译通过的代码了。
- 之后:你真正开始享受它了。你更加信任自己的代码。你重构时不再感到恐惧。
信心加成
在生产环境中使用 Rust 最棒的一点是什么?当你的代码编译通过时,你就有相当大的把握它能正常工作。不是 100% 确定,但比大多数语言的把握要大得多。
我用 Rust 做过一些大型重构,如果换作其他语言,我可能会望而生畏。比如移动代码、更改数据结构、更新函数签名。编译器会告诉你每一个需要修改的地方。你修复所有错误后,它就能正常运行了。
// Before: passing primitives
fn calculate_price(quantity: i32, price: f64) -> f64 {
quantity as f64 * price
}
// After: using a proper type
struct Order {
quantity: i32,
unit_price: f64,
}
impl Order {
fn total(&self) -> f64 {
self.quantity as f64 * self.unit_price
}
}
当我从第一个版本修改到第二个版本时,编译器指出了我调用 calculate_price 的每一个地方。我把它们全部更新了。搞定。没有出现运行时意外。
值得吗?
对于业余项目?也许不值得。人们选择 Python 或 JavaScript 来编写快速脚本是有原因的。
对于需要可靠和快速的生产系统?那绝对值得。你花在学习 Rust 上的时间,会通过减少调试时间、降低生产事故和换来安稳的睡眠而得到回报。
在过去的 18 个月里,我们的 Rust 服务从未发生过与内存相关的崩溃。一次都没有。而我们的 Go 服务呢?以前每周都会遇到。
我的建议
如果你正在考虑学习 Rust,以下是一些对我有帮助的建议:
- 从小处着手。 不要第一天就尝试构建一个 Web 框架。可以先构建一个命令行工具,解析一些文件。首先在简单的场景中熟悉所有权和借用。
- 克制住将所有东西都设置为可变的冲动。 那是你旧的编程习惯在作祟。Rust 默认不可变是有原因的。
- 阅读编译器错误。 是真正地去读它们。Rust 的错误信息是我见过最好的。它们会解释哪里出了问题,并且经常会建议修复方案。
- 当你卡住时,休息一下。 有时候你需要睡一觉,问题就解决了。你的大脑需要时间来重新调整。
总结
是的,Rust 有学习曲线。这是真实存在的。但这条学习曲线并不是为了难而难。Rust 中每一个困难的部分都是为了防止某一类真实的错误而存在的。
一旦你越过了那条曲线,你写出的代码将兼具速度和安全性。你会在编译时而不是生产环境中捕获错误。你的服务能够稳定运行,让你睡得更安稳。
关于 Rust 学习曲线的抱怨,大多来自那些还没有在生产环境中使用过它的人。一旦你用过了,你就会明白为什么存在所有这些严格的限制。老实说,当你再回头去用其他语言时,你甚至会开始怀念它。