当「一个主人」不够用:智能指针
第四章的铁律是「一个值一个主人」。但现实里确实存在「一份数据,几方共用」的场景——比如一棵树,多个父节点都想指向同一个子节点。Rust 的应对不是废除铁律,而是提供几种智能指针(smart pointer)——大白话是「长得像指针、但自带管理逻辑的类型」,把例外情况也装进规则的笼子。认识三个:
Box<T>:最简单的——「把一个值放到堆上」。主人还是一个,只是货从栈搬进了仓库。递归类型(链表、树)离不开它,因为编译器需要知道类型占多大空间。Rc<T>:引用计数——「这个值可以有多个主人,我记着数」。每多一个Rc::clone计数加一,每退场一个减一,数到零才真正归还内存。注意Rc::clone只是计数加一,几乎免费,和第四章那个深拷贝的.clone()不是一回事。代价:计数是运行时开销,而且 Rc 只能在单线程用——多线程场景编译器直接拒。RefCell<T>:「借用规则挪到运行时检查」。编译期说不清的借用关系,它让你在运行时动态核验——违规不再编译不过,而是运行时 panic。安全网还在,只是从编译期改成了运行时值班。
看出设计哲学了吗?规则一条没废,只是每条规则都提供了「编译期版」和「运行时版」,由你按场景选。默认走编译期(零开销),真有需要就用智能指针换成运行时检查(花小钱)。
无畏并发:编译器替你盯住线程
现在上终章的正题。并发——多个线程同时跑——是 bug 密度最高的编程领域,因为经典事故数据竞争(data race)太隐蔽:两个线程同时改同一块内存,结果取决于谁跑得快,不可复现、难以排查。
但在 Rust 里写多线程,你会发现一件惊人的事:数据竞争根本编译不出来。原理你已经全学过了——第五章的借用排他规矩(能看不能摸,能摸别人就不能看)在线程间照样生效,而把它搬到线程世界的,是两个特殊的标记 trait:Send(这个值能安全地过户给别的线程)和 Sync(这个值能安全地被多个线程同时看)。编译器为每个类型自动判定这两个标记;不满足的类型(比如 Rc)一旦被线程共享,当场拒编译。
use std::thread;
let v = vec![1, 2, 3];
thread::spawn(move || {
println!("{:?}", v); // move 闭包:v 的所有权过户进线程
});
// 此处再用 v 就编译错误——它已经属于那个线程了
要在多个线程间共享数据,用线程安全版的智能指针组合:Arc<Mutex<T>>。Arc 是「原子引用计数」,Rc 的多线程版;Mutex 是互斥锁——同一时刻只放行一个线程碰数据,想改先拿锁,拿到的是一张「临时可改许可证」,用完自动还锁:
use std::sync::{Arc, Mutex};
use std::thread;
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
let c = Arc::clone(&counter); // 计数加一,几乎每个线程一份「共管凭证」
handles.push(thread::spawn(move || {
let mut n = c.lock().unwrap(); // 拿锁,许可证到手
*n += 1; // 十个线程各加一次
}));
}
for h in handles { h.join().unwrap(); }
println!("{}", counter.lock().unwrap()); // 必然是 10,分毫不差
这段代码的每一行都在执行旧规则:所有权过户给线程、共享靠计数、修改先拿锁——编译器全程核验。写漏了 lock() 直接编译不过。这就是「无畏并发」(fearless concurrency)的真实含义:不是并发变简单了,而是最危险的那类错误被提前挪到了编译期,和内存安全是同一个赌注的第二次兑现。
入门之后去哪
这本书到此为止——你已经有能力读懂大部分 Rust 代码,也有概念框架去接住剩下的东西了。给你三个方向:
- 官方《Rust 程序设计语言》(俗称 "The Book"):免费在线,现在你读它会非常顺,因为概念的地基已经打好,它能补上我们略过的细节。
- Rustlings:一组刻意带错的小练习,修到编译通过为止。被编译器追着改几十次,手感就真的长出来了。
- 写点什么:一个命令行小工具、一个猜数字游戏的升级版、一个读日志的脚本。用
cargo add装上 serde(序列化)、clap(命令行参数)、tokio(异步)这三个生态里最常见的库,你就踏进了真实世界。
最后一句话留给那个贯穿全书的问题:Rust 凭什么不用垃圾回收器也担保内存安全?现在你有了自己的答案——它把「谁拥有、谁能借、活多久、能不能跨线程」四件事全部变成编译期可核验的规则,然后由一个从不疲惫的编译器替你执行。代价是你写代码时要多回答几个问题;回报是你的程序在凌晨三点不会崩。这位最严格的朋友,值得交。