一句话总结
借用(Borrow)让不夺走所有权地访问数据成为可能,通过引用 &T(只读)和 &mut T(可写)实现.
借用检查器有一条铁律:共享不可变,可变不共享.它在编译期消灭了数据竞争与悬垂引用.
切片(&[T],&str)则是借用集合一部分的"窗口".
解决所有权的"繁琐"
上一篇结尾的痛点:只想读一下数据,却要把所有权传进函数再传回来.借用就是为此而生--它让函数能访问数据而不拥有数据.
|
&s 创建一个指向 s 的引用,函数借用了 s 但不拥有它;函数结束,引用消失,s 安然无恙.
所有权是把书送人,借用是把书借给人看.
借阅者能读,读完归还,书还是原来的.Rust 的 & 就是那张借书凭证.
可变借用
默认引用是只读的.想通过引用修改数据,需要可变引用 &mut,且原变量本身必须是 mut:
|
借用规则:全篇核心
可变引用能力更强,Rust 因此施加一条核心约束.这是本系列最该刻进肌肉记忆的规则:
借用检查器的铁律
在任意给定的作用域内,对同一份数据,要么:
- 存在任意多个不可变引用
&T(共享只读), - 要么存在恰好一个可变引用
&mut T(独占读写).
两者不能同时存在.一句话:共享不可变,可变不共享.
|
但下面这样就会被拒绝:
|
这条规则在编译期消灭了数据竞争(data race).回想 Go:多个 goroutine 同时读写一个 map 会 panic 或未定义行为,得自己加 sync.Mutex 或靠 race detector 在运行时发现.Rust 把这类错误提前到了编译期.
把数据想象成共享文档:可以很多人同时阅读(多个 &T),但一旦有人要编辑(&mut T),就必须独占,其他人连读都不行.
这样永远不会出现"边改边读"的脏数据.
NLL:借用在"最后一次使用"后就结束
有个让规则更人性化的细节,叫非词法生命周期(NLL,Non-Lexical Lifetimes):一个引用的"借用期"不是持续到作用域大括号结束,而是到它最后一次被使用为止.
|
为什么要知道 NLL
很多新手会困惑"明明先用完了不可变引用,为什么报错"--其实多数情况下 NLL 已经放行.
理解"借用到最后一次使用为止",能帮助看懂为什么有些看似冲突的代码其实合法,也能指导把引用的使用尽量集中,尽早用完来避免借用冲突.
借用规则的副产品:杜绝悬垂引用
借用检查器还顺手解决了另一类经典 bug.C 里可能返回指向局部变量的指针,函数结束后该内存失效,指针悬空.Rust 直接拒绝编译:
|
编译器发现:返回的引用指向的数据会在函数结束时释放,于是直接报错.正确做法是返回 String 本身(移交所有权),让数据活得足够久.
这里埋了一颗种子:生命周期
报错提到 "lifetime"(生命周期).引用必须活得不比它指向的数据长--编译器如何精确推断这一点,就是生命周期要解决的问题.
本系列先建立"引用不能比数据活得久"的直觉,深入机制留到「Rust 进阶 · 生命周期与并发」分类.
切片:借用集合的一部分
切片(slice)是一种特殊的引用:它借用集合中连续的一段,而不拥有数据.类型写作 &[T](数组/Vec 切片)和 &str(字符串切片).
|
切片像给集合开了一扇窗:能透过窗看到,操作其中一段数据,但窗本身不持有房子.
窗(切片)的存在期间,房子(原集合)不能被拆(被移动或修改到使切片失效),这同样由借用规则保证.
最佳实践:函数参数优先用切片
一个非常地道的 Rust 习惯:函数接收字符串参数时,优先用 &str 而非 &String;接收序列时优先用 &[T] 而非 &Vec<T>.因为切片更通用--它能同时接受多种来源:
|
为什么这是最佳实践
&String 只能接受 String 的引用;而 &str 能接受 String,字面量,切片等所有"字符串视图".用更通用的切片类型做参数,函数适用面更广,调用方更省心.
同理,序列参数用 &[T] 而非 &Vec<T>.这是 Clippy 也会提示的地道写法.
快速回顾
- 借用:用
&T(只读)/&mut T(可写)访问数据而不取得所有权,解决所有权往返的繁琐. - 借用铁律:同一数据,要么多个
&T,要么单个&mut T,二者不并存--编译期消灭数据竞争. - NLL:借用到"最后一次使用"为止,而非作用域末尾;尽早用完引用可避免冲突.
- 悬垂引用:返回指向局部变量的引用会被编译器拒绝(引出生命周期概念).
- 切片:
&[T]/&str借用集合连续的一段,是一种不拥有数据的"窗口". - 最佳实践:函数参数优先用
&str/&[T]而非&String/&Vec<T>,更通用.
动手练习
- 可变借用:写一个
fn append_suffix(s: &mut String)在字符串末尾追加内容,在main中调用并打印. - 规则冲突:在同一作用域同时创建一个
&mut和一个&引用,记录报错,并解释这条规则如何防止数据竞争. - NLL 验证:构造一个能体现 NLL 的例子--先用完不可变引用,再创建可变引用,验证它能编译通过.
- 悬垂引用修复:写一个返回局部
String引用的函数,观察 "lifetime" 报错,再改成返回String修复. - 切片通用性:写一个函数,参数用
&str,分别用String和字符串字面量调用它,体会切片参数的通用性.