卷 I · 门槛CH 01深度 01/24

你被劝退过几次

先不急着学。我们先做一件别的书不做的事:把劝退过你的那几堵墙,一堵一堵摆到桌面上,看清楚它们长什么样。因为接下来我要证明给你看 —— 它们不是五堵墙,是同一堵墙的五个侧面。看穿这一点,你就已经上了一半的岸。

劝退所有权心态全书地图

我们都是从同一个地方掉下来的

故事的开头总是很顺。你听说了 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 偏不:对于 StringVec 这类值,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 代码看起来「啰嗦」:到处是 ResultOption?match.unwrap()RcArcMutex。别的语言一行搞定的事,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 不是学一套全新的怪规矩。是把你本来就在做、只是没意识到的那套内存纪律,第一次摊开、说清、并交给机器去查。

这一章的一句话

你不是被五个难点分别击败,你是被同一个还没建立的心智模型 —— 所有权 —— 绊倒了五次。接下来整本书,就是把这个模型,一块砖一块砖,稳稳地垒进你脑子里。

下一章,我们先跟那个把你劝退了无数次的对手 —— 编译器 —— 握手言和。