一个值几十亿美元的小错误
2024 年 7 月,一家安全公司的一次更新让全球八百多万台 Windows 电脑集体蓝屏,航班停飞、医院停摆。事故的直接原因,事后报告写得很清楚:一段用 C++ 写的代码读了一块它不该读的内存。没有外星人,没有玄学,就是程序世界里最古老的那种错误——手伸过了界。
这类错误有个共同的家族名字:内存安全问题,大白话就是「程序碰了不该碰的内存」。它有几个经典形态:数组越界(第 11 个位置去读只有 10 个元素的数组)、悬垂指针(内存已经还给系统了,代码里还留着指向它的箭头,像房子拆了还按旧地址寄信)、重复释放(同一块内存还了两次)。
这个家族有多常见?微软和 Google 的 Chrome 团队各自统计过自己的安全漏洞,结论几乎一样:大约七成的严重安全漏洞,根子都是内存安全问题。不是程序员笨——是这门语言把一件机器更擅长做的事,交给了人。
两条老路,都有代价
过去几十年,业界对这个问题的回答基本只有两种。
第一条路:C/C++ 的手动管理。内存由程序员自己申请、自己归还。好处是快——没有任何多余的开销,代码干什么、什么时候干,一目了然。坏处是全靠人不出错,而人是会出错的。于是有了悬垂指针、内存泄漏,以及那七成漏洞。
第二条路:垃圾回收。Java、Python、Go 这些语言的答案是:别让人管了,让语言运行时(runtime,可以理解为程序背后一个一直值班的管家)自动回收没人用的内存。这一下就安全了——管家盯着,悬垂指针几乎不可能出现。但管家不是免费的:它要定期停下来清点全场(这就是游戏里偶尔卡一下、服务里偶发的延迟毛刺的来源之一),还要留出富余内存才能高效工作。
两条路的本质分歧是:安全检查在什么时候做、谁来做。C 说「编译期不查,运行时也不查,程序员自己保证」;GC 语言说「运行时有个管家实时查」。前者快但危险,后者安全但要养一个管家。
Rust 的赌注:编译期把账算清
Rust 提出了第三条路,也是这本书要一步步讲清楚的那条路:安全检查不放到运行时,而是挪到编译期,由编译器做。
想法很朴素:程序跑起来之前的编译阶段,编译器就已经把整份代码通读了一遍。如果它能在这时候就证明「这块内存在这里只有一个主人」「这个箭头指向的东西一定还活着」,那运行时就根本不需要管家——程序以 C 一样的裸速度运行,同时内存安全是编译器用数学般的规则担保过的。
这套规则就是后面几章的主角:所有权(ownership)。先剧透一句它的核心思想,精确到令人发指:
- 每块内存(每个值)在任一时刻只有一个主人;
- 主人离开作用域,内存立刻自动归还——不用手动 free,也不用等管家;
- 别人可以「借用」,但借用的规矩由编译器强制执行,违规就编译不过。
类比:C 像把自家钥匙随便复印分发,丢了东西自己负责;GC 语言像请了个宿管阿姨,随时巡查,安全但楼里要给她留个值班室;Rust 像一套门锁系统——钥匙的每一次交接都被登记,谁拿了、拿了多久、能不能进,进门之前系统就核验过,核验不过门根本打不开(程序根本编译不出来)。
代价是什么:一个严格的朋友
天下没有免费的午餐。Rust 把检查挪到编译期,代价是你必须在写代码的时候就向编译器讲清楚内存的来龙去脉。你的代码不再是写给人看的随笔,而是写给一个极其较真的审计员的报告。
所以 Rust 新手都有同款经历:一段在 Python 里顺手就写完的代码,在 Rust 里被编译器打回三次。报错信息密密麻麻,什么 "borrowed value does not live long enough"(借来的东西活得不够久)。头两周你可能会觉得这语言在跟你作对。
但换个角度:编译器拦下的每一个错,都是别的语言里一颗定时炸弹——它在 C++ 里会变成半年后一次无法复现的崩溃,在 Java 里会被运行时兜住但拖着性能陪葬。Rust 只是让你提前、在椅子上、看着明确报错把它拆掉。很多老 Rust 程序员有个共同体验:代码只要编译通过,跑起来往往就是对的。这本书的副标题说编译器是「最严格的朋友」,道理就在这——它严格,是因为它替你把了关。
这本书怎么读
接下来我们会先花十分钟把环境装好、把第一个程序跑起来(第二章),然后从「变量默认不许变」这个最小的规则开始,一层层推到所有权、借用、生命周期——那是 Rust 的心脏(第三到六章)。之后再讲结构体、枚举、错误处理这些日常工具(第七到十章),最后看 trait、泛型和并发,看这套规则如何在最复杂的场景里兑现「无畏并发」的承诺(第十一、十二章)。
贯穿全书的问题是同一个:Rust 凭什么不用垃圾回收器,也能担保内存安全?它具体怎么做到的,你要为此付出什么?读完这本书,你不仅能写出能编译的 Rust,更重要的是,你会真正明白编译器每次拦下你时,它到底在保护什么。