你被劝退过几次
先不急着学。我们先做一件别的书不做的事:把劝退过你的那几堵墙,一堵一堵摆到桌面上,看清楚它们长什么样。因为接下来我要证明给你看 —— 它们不是五堵墙,是同一堵墙的五个侧面。看穿这一点,你就已经上了一半的岸。
我们都是从同一个地方掉下来的
故事的开头总是很顺。你听说了 Rust —— 安全、快、没有 GC、连续好几年蝉联「最受喜爱的语言」,Linux 内核收了它,各大公司都在招人。你决定学。装工具链、跑 Hello World、打印个斐波那契,一切丝滑,你想「不过如此嘛」。
然后你写了第一段有点内容的代码。比如把一个字符串传给两个函数:
let name = String::from("Ferris");
greet(name);
log(name); // ← 就在这里,一切开始崩塌
编译器甩给你一大段红字:error[E0382]: use of moved value: `name`。你一脸问号:我就是用了一下这个变量,怎么就「moved」了?它又没动,还在那儿呢。你上网搜,看到「所有权」「移动语义」,看得云里雾里;你试着加 &、加 .clone()、加 .to_string(),编译过了,但你不知道自己为什么对了。
再往后,坑越来越密。你想写个函数返回一个引用,编译器要你标 <'a>,你照抄了但完全不懂那是什么;你想在结构体里存另一个对象的引用,掉进生命周期的泥潭;你想两处代码都能改同一个列表,撞上「一个可变借用」的南墙;你终于放弃优雅,全给套上 Rc<RefCell<T>>,写出一堆连自己都嫌丑的代码。某个深夜,你关掉编辑器,心里冒出那句熟悉的话:
「Rust 是好,但我可能不是那块料。」
我想先说清楚一件事:这句话是错的。被劝退跟聪不聪明没关系。你卡住,是因为几乎所有 Rust 教程都在同一个地方、用同一种方式,让你在还没理解地基的时候,就去盖房子。房子当然会塌。这一章,我们把地基先挖出来给你看。
五堵墙,逐一点名
把大家掉下去的坑列出来,你会发现它们出奇地一致。
墙一:赋值一下,原来的变量就「没了」
这是第一个、也是最反直觉的冲击。在你熟悉的所有语言里,b = a 之后 a 都还好端端在那儿。Rust 偏不:对于 String、Vec 这类值,let b = a; 之后 a 就作废了,再用它就是 E0382。
你的第一反应是「这什么破设计」。但记住这个疑问 —— 到第 4 章你会发现,这恰恰是 Rust 敢承诺「没有 GC 也不会有内存 bug」的起点。它不是故意为难你,是在回答一个 C 程序员用二十年才学会小心的问题:这块内存,到底该由谁来释放?
墙二:不让你「一边读一边改」
你想遍历一个列表,同时往里加元素;或者一个函数拿着某个数据的可变引用,另一个地方又想读它。编译器拦住你:cannot borrow as mutable because it is also borrowed as immutable。你觉得莫名其妙 —— 这不是天天都在干的事吗?
是的,天天都在干,也天天在出 bug。「一边遍历一边修改」是无数语言里 ConcurrentModificationException、迭代器失效、野指针的来源。Rust 只是把这个「你一直在冒的险」变成了编译期就拦下的错误。这就是那条贯穿全书的铁律:要么很多人一起读,要么一个人独自写,不能同时。
墙三:尖括号里的 'a
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str —— 第一次见到这行,几乎每个人都懵。这个撇号加字母是什么?为什么函数签名突然多了一堆符号?
生命周期是 Rust 里被误解最深的东西。它看起来像一种你要「发明」出来的复杂类型参数,其实它什么都不是 —— 它只是让你把一句大白话说给编译器听:「我返回的这个引用,不会比传进来的参数活得更久」。编译器本来就在算这件事,大多数时候还能自己算出来(所以你平时根本不用写);只有它算不清、需要你表个态的时候,才让你标一下。第 8 章我们用一张图把它彻底讲明白。
墙四:String 还是 &str?到底用哪个
你想写个函数接收字符串,参数该写 String、&String 还是 &str?返回值呢?为什么字面量 "hi" 是 &str,而 String::from("hi") 是 String?光是「字符串」就有两个类型,已经够让人烦躁了。
这个困惑,本质上还是墙一和墙二的延伸:一个是「拥有」那串字符,一个是「借看」那串字符。一旦你脑子里装上「拥有 vs 借用」这根弦,String 和 &str 的选择会变得像呼吸一样自然。第 9 章专讲。
墙五:满屏的仪式感
Rust 代码看起来「啰嗦」:到处是 Result、Option、?、match、.unwrap()、Rc、Arc、Mutex。别的语言一行搞定的事,Rust 好像要写三行。你觉得这门语言「不让人好好写代码」。
但这些「仪式」,每一个都在替你把一类运行时崩溃变成编译时提醒:Option 消灭了空指针,Result 让你不可能忘记处理错误,? 又把处理错误的样板压回一个字符。等你习惯之后回头看别的语言,你会开始怀念这份「啰嗦」—— 因为它换来的是「编译过了,我就基本不用怕它半夜崩」。
现在,把五堵墙叠在一起
把上面五条并排放,盯着看一会儿,你会看到它们背后是同一个东西在反复出现:
| 你撞的墙 | 它其实在管的事 |
|---|---|
| 移动后不能再用(墙一) | 这个值现在归谁 |
| 不能同时读和改(墙二) | 此刻谁有权读、谁有权写 |
| 生命周期 'a(墙三) | 借出去的东西,会不会比主人活得久 |
| String vs &str(墙四) | 拥有一份,还是只借看一眼 |
| Result / Rc / Mutex(墙五) | 所有权规则在错误、共享、并发下的各种展开 |
看出来了吗?它们全是「所有权」这一件事的不同侧面。你不是被五个难点各个击破,你是被同一个你还没建立起来的心智模型,绊倒了五次。
大多数教程把「所有权」当成众多章节里的一章,夹在语法和标准库之间讲。这本书赌的是:所有权不是一章,它是整门语言。所以我们花整整一卷(卷 II,六章)只讲它,慢慢讲、动手玩,把地基夯到你闭着眼都清楚「此刻这个值归谁、谁能碰它」。
地基一旦稳了,你会有种奇怪的体验:卷 III 之后的东西 —— 枚举、trait、泛型、并发、async —— 反而变简单了。因为它们都只是在这块地基上盖房子,而你终于看得懂图纸了。
为什么是这个顺序:约束在前,自由在后
Rust 和别的语言,学习曲线的形状是反的。
Python、JS 这类语言的曲线,是「先甜后苦」:上手极快,你很快就能写出能跑的东西;但随着项目变大,那些当初被省掉的严谨 —— 谁改了这个全局状态、这个 null 从哪来的、这两个线程为什么打架 —— 会在运行时、在生产环境、在半夜一笔一笔向你讨债。
Rust 的曲线是「先苦后甜」:它把这些账全部提前到编译期,一次性摊在你面前,要你在代码能跑之前就说清楚。这个开头当然难受 —— 你被劝退,就是难受在这里。但这笔苦是一次性的:付清了,你换来的是一种别的语言给不了的踏实 —— 「cargo build 过了」这句话,在 Rust 里的分量,比在任何语言里都重。
几乎每个人都在同一个地方情绪崩溃:为了让编译器闭嘴,把代码改得面目全非,改完还是错,而且完全不知道自己在干嘛。这时候你会觉得是在和一个刁钻的机器人斗智斗勇。
这本书想扭转的正是这个瞬间。到第 7 章你会有一台能当场判、还告诉你为什么的借用检查器可以折腾。当你能预测它会怎么判、并且理解它为什么这么判,那种「和机器人搏斗」的感觉就消失了 —— 剩下的是「哦,它说得对」。那一刻,你就过墙了。
这一卷剩下的两章,我们做什么
在正式挖地基(卷 II)之前,卷 I 还剩两件事要办好,都是心态层面的,但很重要:
- 第 2 章:换一个视角看编译器。它不是拦路的敌人,而是你的第一个、也是最较真的代码审查员。它拦下的每一条错误,都对应着别的语言里一次真实的线上事故。学会读它的话,比学任何语法都值。
- 第 3 章:花五分钟把工具链装好,然后把它忘掉。Rust 的工具链(
rustup+cargo)好用到几乎不需要你操心,我们快速过一遍,好把全部注意力留给真正难的东西 —— 语言本身。
回流
Rust 逼你显式想的那些事,你在别的语言里其实一直在想,只是靠「习惯」和「小心」在维持,没人帮你查。
Java / Kotlin:你写过 Collections.unmodifiableList 吗?防御性拷贝呢?你在 code review 里揪过「这个对象被两个地方持有,一个改了另一个没料到」的 bug 吗?那就是所有权和借用问题,只不过 JVM 不帮你查,靠你和同事的肉眼。
C / C++:「这块内存谁负责 free」「这个指针指向的对象还活着吗」—— 这正是 Rust 的所有权和生命周期,只是 C 让你全靠脑子记,记错就是段错误或安全漏洞。Rust 把这套心智模型搬进了类型系统,让编译器替你记。
所以学 Rust 不是学一套全新的怪规矩。是把你本来就在做、只是没意识到的那套内存纪律,第一次摊开、说清、并交给机器去查。
这一章的一句话
你不是被五个难点分别击败,你是被同一个还没建立的心智模型 —— 所有权 —— 绊倒了五次。接下来整本书,就是把这个模型,一块砖一块砖,稳稳地垒进你脑子里。
下一章,我们先跟那个把你劝退了无数次的对手 —— 编译器 —— 握手言和。