Rust 入门:编译器是你最严格的朋友 书架

第 05 章 / 共 12 章

第五章 · 借用:能看不能摸,能摸别人就不能看

过户太狠了,能不能只是「借」?

上一章结尾留了个疙瘩:把数据交给函数用一下,就得过户、从此再见?当然不是。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

过户与借用,两张牌你都见过了。但刚才拒编译的那段代码引出一个新问题:编译器是怎么判断「纸条的有效期」的?判断不出来的时候,为什么有时要我们手写一种叫「生命周期」的标注?下一章解开。

Rust 入门:编译器是你最严格的朋友 · 王建硕
费曼式写法 · 由多个 AI 代理撰写与互相审校