过户太狠了,能不能只是「借」?
上一章结尾留了个疙瘩:把数据交给函数用一下,就得过户、从此再见?当然不是。Rust 给的方案叫借用(borrowing):函数拿到的是一个引用(reference)——大白话是「一张写着地址的纸条」,看一眼货物在哪,但货物的主人没变,用完纸条作废,东西还是你的。
fn main() {
let s = String::from("你好");
let n = length(&s); // 把「纸条」递给函数,s 还是主人
println!("{} 有 {} 个字节", s, n); // s 照常用
}
fn length(text: &String) -> usize {
text.len()
} // 纸条作废,货物纹丝不动
&s 读作「s 的引用」。函数签名里的 &String 表示「我收一张指向 String 的纸条,不过户」。因为不过户,函数里就不能把人家的东西据为己有——比如 text 离场时不会触发 Drop,货物不会被还掉。
两种纸条:能看的,和能改的
默认的引用是只读的。想借来改,得用可变引用 &mut,而且原变量本身也得是 mut 的——改别人的东西,既要主人有「可改」的资格,也要你明说「我借来是要改的」:
fn main() {
let mut s = String::from("你");
push_hi(&mut s);
println!("{}", s); // 你好
}
fn push_hi(text: &mut String) {
text.push_str("好");
}
到这里都还顺理成章。接下来是 Rust 借用规则的真正核心,也是无数新手被编译器拦住的第一关:
- 同一时刻,要么有任意多张只读纸条;
- 要么只有一张可改纸条;
- 两者绝不能同时存在。
let mut s = String::from("hi");
let r1 = &s; // 只读纸条一
let r2 = &s; // 只读纸条二:没问题
let m = &mut s; // 想再拿一张可改纸条
println!("{} {} {}", r1, r2, m); // 编译错误!r1 在这里还在生效,
// 只读与可改同时存在,拒发
类比:一份共享文档。只读引用像「查看权限」——发给一百个人同时看都没事;可变引用像「编辑权限」——要发就只能发给一个人,而且发之前必须把所有人手里的查看权限收回来。否则就会出现:你正读到一半,别人把你读的那行改了——你的「阅读」瞬间变成了不可靠信息。
这条规矩在日常单线程代码里防的是「迭代时改集合」这类隐蔽 bug;但它真正的威力在并发里——第十二章你会看到,多个线程同时读写同一块数据(data race,数据竞争,并发 bug 之王)在 Rust 里根本编译不出来,靠的就是这条规矩在编译期的强制执行。写代码时多啰嗦两句,换来一整类最头疼的 bug 从世界上消失。
还有一个好消息:这个检查不是死板的「作用域内都算数」。编译器看的是纸条最后一次被用到哪里(这个机制叫 NLL,非词法生命周期——大白话:纸条的「有效期」算到你最后一次用它的那一行为止,而不是死撑到花括号结束)。所以用完只读纸条之后再去申请可改纸条,是允许的。
编译器亲手掐死悬垂指针
第一章的悬垂指针——房子拆了还寄信——在 Rust 里是什么下场?试试:
fn dangle() -> &String {
let s = String::from("临时");
&s
} // 编译错误!s 在这行被销毁,返回的纸条将指向一片废墟
函数想返回一张指向局部变量的纸条,但局部变量在函数结束时就归还内存了。编译器一眼看穿:这张纸条出了函数就是废纸一张,直接拒编译。在 C 里,这段代码编译畅通无阻,然后在某天深夜的生产环境里炸给你看。
这就是「严格的朋友」的完整工作方式:它不猜你的意图,不给你留情面,但它拦下的每一刀,刀刀都砍在真实存在的事故上。被它拦下时不烦,是入门的标志。
顺路认识 &str
日常写代码你会频繁见到 &str(读作「字符串切片」):它是「指向一段字符串内容的只读纸条」。字符串字面量 "你好" 就是这个类型。很多函数签名会写成收 &str 而不是 &String,因为它更通用——String 的纸条能自动转成它,字面量也能直接用。记住这个手感:只读用别人的字符串,参数类型写 &str。
过户与借用,两张牌你都见过了。但刚才拒编译的那段代码引出一个新问题:编译器是怎么判断「纸条的有效期」的?判断不出来的时候,为什么有时要我们手写一种叫「生命周期」的标注?下一章解开。