12-引用与借用
26 分钟阅读
引用与借用 (References and Borrowing)
面向 Rust 1.97.1 (stable, 2026-07)。本篇假设你熟悉 Go,凡有可比性处均给出 🐹 Go 对比。
热度:
hot高频 |common常见 |occasional偶尔 |advanced进阶少见
本篇能解决什么:
- 你会不会把
&T/&mut T当成 Go 的普通指针,结果一改就撞上error[E0502]/error[E0499]? - 你是否想知道:为什么
HashMap::get之后不能立刻insert,Go 里却很自然? - 你是否分不清 NLL(Non-Lexical Lifetimes,非词法生命周期)到底放宽了什么、没放宽什么?
- 你是否写过
for x in &v { v.push(...) },却不懂编译器在防什么真实内存错误? - 你是否需要一套「先缩作用域 / 再换 API / 最后才 clone 或内部可变」的日常判断流程?
术语速查表:
| 术语 / 缩略词 | 全称 / 读法 | 中文 | 一句话解释 | Go 里的近亲 |
|---|---|---|---|---|
| reference | — | 引用 | 指向某值的借用指针,不拥有该值 | *T / 切片头,近似 |
| borrow | — | 借用 | 临时只读或可写访问,但不接管所有权 | 传指针 / 传切片头 |
| shared borrow | &T | 共享借用 | 可同时存在多个的只读借用 | 多个只读指针,近似 |
| mutable borrow | &mut T | 可变借用 | 同一时刻只能有一个的独占可写借用 | 无编译期对应 |
| borrow checker | — | 借用检查器 | 编译期检查引用别名与生命周期的规则引擎 | Go 无 |
| NLL | Non-Lexical Lifetimes | 非词法生命周期 | 借用活到最后一次使用,而不必活到 } | Go 无 |
| reborrow | — | 再借用 | 从已有借用上临时再借一层,内层结束后外层恢复 | Go 无直接对应 |
| lifetime | — | 生命周期 | 引用允许存在的时间范围(类型系统里的标注) | Go 无 |
| dangling reference | — | 悬垂引用 | 指向已释放内存的引用 | 悬空指针 |
| Deref | — | 解引用强制转换 | 如 &String 自动当 &str 用 | Go 无同名机制 |
| interior mutability | — | 内部可变性 | 通过共享引用仍能改内部数据(运行时检查) | 无直接对应 |
Cell<T> | — | 按值内部可变 | 共享下整值替换 / Copy 读写,不借出引用 | 无 |
RefCell<T> | — | 运行时借用检查 | 共享下借出 &T/&mut T,违规则 panic | 无 |
'static | — | 静态生命周期 | 能活到程序结束的引用生命周期 | 包级变量,近似 |
T: 'static | — | 静态约束 | 类型内部不含短生命周期借用 | 拥有数据的值,近似 |
| owner | — | 所有者 | 负责最终释放该值的绑定 | 无(Go 靠 GC) |
| GC | Garbage Collector | 垃圾回收器 | 运行时回收不用对象 | Go 默认机制 |
| RAII | Resource Acquisition Is Initialization | 资源获取即初始化 | 资源随作用域结束自动释放 | defer 手动模拟 |
| UB | Undefined Behavior | 未定义行为 | 语言不保证结果的非法操作 | 数据竞争等 |
说明:本表覆盖本篇出现的所有专业名词与缩略词;正文首次出现时仍会就地解释一次。
热度索引:
| 热度 | 题目 |
|---|---|
hot | Q1, Q2, Q3, Q4, Q5, Q21 |
common | Q6, Q7, Q8, Q9, Q10, Q11, Q12, Q13, Q14, Q15, Q22 |
occasional | Q16, Q17, Q18 |
advanced | Q19, Q20 |
Q1. &T 和 &mut T 的底层规则到底是什么?
Tags: hot beginner borrowing
适用版本: Rust 1.0+(1.97.1 行为一致)
一句话答案:
对同一数据,同一时刻要么任意多个共享借用 &T,要么唯一一个可变借用 &mut T——二者不可并存;借用不转移所有权(ownership,所有权,见 11-ownership)。
解答:
&x 创建对 x 的共享借用(shared borrow,类型 &T,只读);&mut x 创建可变借用(mutable borrow,类型 &mut T,独占可写)。两者都不吃掉所有者,用完后原变量仍可用。
硬规则只有一条:
- 可有任意多个
&T,或 - 只能有一个
&mut T
不能同时既有人读又有人改。这在单线程也成立——防的是逻辑别名错误,不只是数据竞争。
| |
共享借用存活期间不能再拿可变借用:
| |
函数参数用借用时,调用方继续拥有值:
| |
Go 对比:
| |
- Go 怎么做:传
*T很常见,别名安全靠约定和 race detector。 - Rust 为什么不同:把「能否同时读写同一值」提前到编译期的借用检查器(borrow checker)。
- Go 程序员易踩的坑:把
&mut当成普通指针;它是带独占承诺的借用,不是“随便传的地址”。
记忆点:
&T= 只读访客证(可多张);&mut T= 独占施工证(同时只能一张)。- 借用 ≠ move:所有者还在。
E0502= 共享与可变冲突;E0499= 两个可变冲突。
Q2. 为什么不能同时拿两个可变借用?
Tags: hot beginner mut-borrow
适用版本: Rust 1.0+;NLL 改善诊断但独占规则不变
一句话答案:
&mut 承诺「我是此刻唯一写入者」;两个同时存活的可变借用会破坏该承诺,编译器报 error[E0499]。
解答:
经典踩坑:想同时改 v[0] 和 v[1]。&mut v[i] 在类型系统里是先对整个 v 做可变借用再索引——借用检查器不做「下标不同就不重叠」的值级证明。
| |
顺序使用:上一个 &mut 用完后再借下一个——在 NLL(Non-Lexical Lifetimes,非词法生命周期,详见 Q5)下自然合法:
| |
需要同时持有两段不重叠的可变切片时,用标准库 API(内部用 unsafe 证明不重叠,对外安全):
| |
更多见 Q14。
Go 对比:
| |
- Go 怎么做:切片元素地址可同时持有;安全靠程序员约定。
- Rust 为什么不同:默认拒绝「整容器上的双重可变别名」,除非你用证明过不重叠的 API。
- Go 程序员易踩的坑:以为「下标不同」编译器会自动放行——不会。
记忆点:
- 两个活着的
&mut同一数据 →E0499。 - 先用完再借下一个;或
split_at_mut/get_disjoint_mut。 - 不要自己写原始指针“拆别名”,优先标准库。
Q3. 为什么“先借后改”会报 E0502?
Tags: hot beginner E0502
适用版本: Rust 1.0+
一句话答案:
共享借用还活着时再要可变借用,就是 error[E0502];先结束只读借用(或先拷贝出需要的值),再改。
解答:
「先借后改」指:手里还拿着 &T,又对同一所有者调用需要 &mut 的操作。
| |
修法一:读完再用(NLL 下借用止于最后一次使用):
| |
修法二:先把需要的数据拷成拥有值,再改容器:
| |
Go 对比:
| |
- Go 怎么做:编译器通常不拦「边持指针边扩容」。
- Rust 为什么不同:
E0502正是在编译期挡住「引用还活着却让底层失效」的路径。 - Go 程序员易踩的坑:看到
E0502以为编译器“太严”;它常在防真实的悬垂。
记忆点:
Q4. 为什么 HashMap::get 之后不能立刻 insert?
Tags: hot intermediate hashmap
适用版本: 全版本;NLL 有时仍不够(引用还被后续使用)
一句话答案:
get 返回的引用生命周期绑在整个 map 上;insert 需要 &mut map 且可能 rehash,使旧引用失效——故与存活的共享借用冲突(E0502)。
解答:
| |
常见修法:
| |
若只是「没有则插入」,优先 entry,避免手搓 get+insert 两阶段:
| |
Go 对比:
| |
- Go 怎么做:
m[k]对小类型常是值拷贝;对指针/切片仍可能共享,靠约定。 - Rust 为什么不同:
get默认借出&V,与可能触发 rehash 的insert冲突。 - Go 程序员易踩的坑:把
get想成“取出一份副本”;对非Copy值它往往是借用。
记忆点:
Q5. NLL(非词法生命周期)到底帮了什么忙?
Tags: hot intermediate nll
适用版本: NLL 自 1.31(2018 edition)引入,1.36 起所有 edition 默认;1.97.1 无需开关
一句话答案:
NLL(Non-Lexical Lifetimes,非词法生命周期)让借用活到最后一次使用,而不必拖到外层 };它不放宽别名规则,只是更精确地结束借用。
解答:
旧词法模型:借用活到作用域花括号结束。NLL:分析控制流,用完即可结束。
| |
NLL 不允许真正的别名可变。下面仍然失败——因为 r 在可变操作之后还被使用:
| |
对「读完再写」的日常代码,NLL 省掉许多多余的 {}:
| |
遇到 Stack Overflow 旧答案说「必须加大括号」时,先在当前版本试一下——许多已不再需要;仍失败时再主动缩作用域(见 Q8)。
Go 对比:
- Go 怎么做:没有借用检查器,也就没有 NLL 这种“精确结束借用”的概念。
- Rust 为什么不同:NLL 是借用检查器的精度升级,不是新语法开关。
- Go 程序员易踩的坑:看到旧文说“必须
{}”,在 1.97 里盲加括号;先确认引用是否还在后面被使用。
记忆点:
- NLL = 活到最后一次使用,不是活到
}。 - 不改变「共享 / 可变互斥」铁律。
- 旧答案里的大括号,很多已过时。
Q6. 函数参数为什么常写成 &str 而不是 &String?
Tags: common beginner deref api
适用版本: Rust 1.0+
一句话答案:
&str 更通用:String、字符串字面量、子切片都能传入(借助 Deref 强制转换);&String 只接受 String 的借用。
解答:
Deref(解引用强制转换)让 &String 在需要 &str 的地方自动转换。API 写成 &str,调用方最省事:
| |
若写成 &String,字面量就不能直接传:
| |
切片同理:参数优先 &[T] 而不是 &Vec<T>。
| |
Go 对比:
| |
- Go 怎么做:
string/[]T本身就是“视图头”,API 很少再套一层“指向 string 的指针”才接受。 - Rust 为什么不同:
String是拥有者,str是 DST(Dynamically Sized Type,动态大小类型)切片;参数用&str对齐“只读视图”。 - Go 程序员易踩的坑:把
String当成 Go 的string,参数写成&String反而把 API 写窄了。
记忆点:
- 只读字符串参数 →
&str;只读序列 →&[T]。 - 需要拥有 / 修改再收
String/&mut String。 - Deref 是便利,不是所有权转移。
Q7. reborrow(再借用)是什么?
Tags: common intermediate reborrow
适用版本: 全版本
一句话答案:
reborrow(再借用)是从已有 &mut T 上临时再借出 &mut T 或 &T;内层结束后外层可变借用恢复,而不是把外层引用 move 走。
解答:
传给函数时经常发生隐式 reborrow,因此可以多次把同一个 &mut 交给不同调用(非同时重叠):
| |
显式写法是 &mut *r:从 r 指向的数据上再借一次,而不是移动 r 本身:
| |
对比:let r2 = r; 对 &mut T 是 move,之后 r 不能再用:
| |
也可把 &mut T 降级再借为 &T(共享期间不能通过原 &mut 写入,见 Q11)。
Go 对比:
- Go 怎么做:指针赋值是复制指针值,没有“可变借用被 move / reborrow”之分。
- Rust 为什么不同:
&mut T不是Copy,必须区分 move 与临时再借,才能维持独占。 - Go 程序员易踩的坑:对
&mut做let y = x后还想用x——应写成&mut *x或继续隐式传参。
记忆点:
- 函数参数上的
&mut传参多半是 reborrow。 - 显式:
&mut *r;移动:let r2 = r。 - 内层活着时外层冻结;内层结束外层恢复。
Q8. 什么时候该把借用缩进更小的作用域?
Tags: common beginner scope
适用版本: 全版本;多数情况 NLL 已够,仍失败时再手动缩
一句话答案:
当引用在控制流上“看起来还活着”、但你逻辑上已用完,或编译器推断不够聪明时,用 { ... } 或尽早结束使用,强制缩短借用。
解答:
NLL 之后,许多“读完再写”已不需要大括号。仍建议主动缩作用域的典型场景:借用变量名在后面还存在、但中间夹了可变操作;或匹配/分支里借用跨度难读。
| |
对比:同一名字一直活到函数尾,中间又改所有者——更容易踩 E0502:
| |
修法:缩小读阶段,或拷贝出需要的数据:
| |
Go 对比:
- Go 怎么做:没有借用作用域概念;最多靠短生命周期变量提高可读性。
- Rust 为什么不同:作用域与使用点共同决定借用何时结束。
- Go 程序员易踩的坑:变量名“还在”就以为借用一定还在——看最后一次使用,必要时再加
{}。
记忆点:
Q9. 为什么借用期间不能让容器扩容?
Tags: common intermediate vec reallocation
适用版本: 全版本
一句话答案:
扩容(如 Vec::push 触发重分配)可能搬迁堆缓冲区,使仍存活的元素引用变成悬垂引用;借用规则在编译期直接禁止这条路径。
解答:
| |
安全模式:先算完要追加的数据,结束借用,再改容器;或只用索引(在固定 len 范围内):
| |
HashMap::insert 可能 rehash,同理——见 Q4。内存布局细节见 15-memory-and-allocation。
Go 对比:
| |
- Go 怎么做:
append是否换底层靠运行时;持有元素指针时扩容是经典坑。 - Rust 为什么不同:直接不让你在借用元素时扩容。
- Go 程序员易踩的坑:把 Rust 的拒绝当成繁琐;它防的正是 Go 里要小心的悬空。
记忆点:
- 持有
&v[i]/map.get结果时 → 别push/insert。 - 先收集、再修改;或改用索引 /
entry。 - 扩容 = 可能搬迁 = 旧引用作废。
Q10. 可变借用为什么不是 Copy?
Tags: common intermediate copy mut
适用版本: 全版本
一句话答案:
&T 是 Copy(多份只读别名合法);&mut T 若可 Copy 就会凭空复制出第二张独占施工证,破坏唯一可变规则,因此赋值是 move。
解答:
共享借用可以复制:
| |
可变借用赋值会移走:
| |
需要“临时再给别人用一下”时用 reborrow(Q7),不要幻想 &mut 能像 & 一样随便复制。
Go 对比:
| |
- Go 怎么做:指针赋值总是复制地址。
- Rust 为什么不同:用“
&mut不可 Copy”维持独占不变式。 - Go 程序员易踩的坑:对
&mut做赋值后继续用旧名。
记忆点:
&T: Copy;&mut T不是。- 赋
&mut= move;临时共享用 reborrow。 - 与 11-ownership 的 Copy 规则一致。
Q11. 共享借用和可变借用谁更“强”?
Tags: common intermediate reborrow
适用版本: 全版本
一句话答案:
可变借用更“强”:可从 &mut T 再借出 &T(降级);反过来不能从 &T 变出 &mut T。共享借用存活时,原 &mut 不能写入。
解答:
降级再借用:
| |
共享还活着时不能写:
| |
“强”不等于“随时可写”——独占期间你可以降级只读,但只读结束前不能再通过可变路径写入。
Go 对比:
- Go 怎么做:
*T没有共享/可变两套类型;写不写靠约定。 - Rust 为什么不同:用类型区分权限,并可单向降级。
- Go 程序员易踩的坑:以为有了
&mut就随时能写,忽略中间临时的&。
记忆点:
&mut→&可以;&→&mut不行。- 降级共享存活 ⇒ 外层写入暂停。
- 权限可以收窄,不能凭空放大。
Q12. 什么是悬垂引用,Rust 怎么阻止它?
Tags: common beginner lifetime dangling
适用版本: 全版本
一句话答案:
悬垂引用(dangling reference)指向已释放的内存;Rust 用生命周期检查禁止返回局部变量的引用等操作(常见 error[E0515])。
解答:
返回局部 String 的引用会在函数返回后失效:
| |
三种合法方向:
| |
返回引用必须来自输入参数或 'static 数据,详见 27-lifetimes。
Go 对比:
| |
- Go 怎么做:逃逸分析 + GC,返回局部地址往往合法。
- Rust 为什么不同:没有 GC 兜底,必须证明引用不超过所有者。
- Go 程序员易踩的坑:把 Go 的“返回 &局部”习惯搬到 Rust。
记忆点:
- 悬垂 = 指向已死数据。
E0515= 想返回局部引用。- 返回:拥有值 /
'static/ 来自参数的借用。
Q13. 为什么结构体里放引用会突然需要生命周期?
Tags: common intermediate lifetime struct
适用版本: 全版本
一句话答案:
结构体字段若是引用,类型必须声明「这个引用能活多久」,否则编译器无法在使用处检查它是否短于所有者——于是出现 <'a>。
解答:
不含引用的结构体不需要生命周期参数;一旦字段是借用,就要标注:
| |
缺少标注时,定义阶段就会被拒绝:
| |
更完整的省略规则与多生命周期见 27-lifetimes。初学建议:能存 String 就别存 &str,少很多标注。
| |
Go 对比:
| |
- Go 怎么做:结构体里放
string/指针很常见,存活靠 GC。 - Rust 为什么不同:引用字段把“别活得比所有者久”写进类型。
- Go 程序员易踩的坑:一在结构体里放
&str就被生命周期吓到——可先改存String。
记忆点:
- 字段是引用 ⇒ 结构体要
<'a>。 Excerpt不能超过其借用的数据。- 能拥有就拥有,生命周期标注可推迟到 27-lifetimes。
Q14. split_at_mut 为什么是标准解?
Tags: common intermediate slice
适用版本: Rust 1.0+;get_disjoint_mut 需 1.86+
一句话答案:
借用检查器不能证明「两个下标不重叠」,但标准库用 unsafe 在实现里证明后,对外提供安全的 split_at_mut——这是同时拿两段 &mut 的标准做法。
解答:
| |
对比手写双重索引可变借用会 E0499(见 Q2)。多个任意下标可用 get_disjoint_mut:
| |
同类还有 split_first_mut、split_last_mut 等。优先这些 API,而不是自己写 *mut。
Go 对比:
| |
- Go 怎么做:子切片天然共享底层数组,可同时写不同区间(重叠时也危险)。
- Rust 为什么不同:默认禁止双重
&mut;split_at_mut把“不重叠”证明封装进标准库。 - Go 程序员易踩的坑:想当然
&mut v[i]两次——请改用拆分 API。
记忆点:
- 同时两段可变切片 →
split_at_mut。 - 任意不重叠下标 →
get_disjoint_mut(1.86+)。 - 不要为这点小事自己写 unsafe。
Q15. Cell / RefCell 什么时候能绕开编译期借用限制?
Tags: common intermediate interior-mutability
适用版本: 全版本;仅单线程。跨线程用 Mutex / RwLock
一句话答案:
需要「外面是共享借用,里面还能改」时用内部可变性(interior mutability):Cell 做整值替换;RefCell 把借用检查推迟到运行时(违规 panic,不是编译错误)。
解答:
| |
RefCell 在共享引用背后修改——适合「结构体方法是 &self,但要改内部缓存」等场景;同时两个 borrow_mut 会 panic:
| |
不要把 RefCell 当第一选择:能改成 &mut self、拆作用域、换 API,就别上运行时检查。
Go 对比:
- Go 怎么做:结构体指针方法里改字段很随意;没有编译期借用位。
- Rust 为什么不同:默认
&self不能改字段;内部可变性是显式退出“编译期借用”。 - Go 程序员易踩的坑:到处
RefCell把 panic 留到运行时——先问能否&mut self。
记忆点:
Cell:Copy/替换;RefCell:运行时&/&mut。- 单线程;多线程 →锁。
- 能编译期解决就别用内部可变。
Q16. 方法接收者 &self / &mut self 怎么选?
Tags: occasional beginner methods
适用版本: 全版本
一句话答案:
只读用 &self;要改字段用 &mut self;要吃掉并拆开用 self。调用 &mut self 时,接收者变量需是 mut。
解答:
| |
若 c 未声明 mut,调用 inc 会报:
| |
命名惯例:into_xxx 消费 self,as_xxx 借用,to_xxx 常复制出新值。
Go 对比:
| |
- Go 怎么做:值 / 指针接收者;调用方常自动取址。
- Rust 为什么不同:
&mut self要求绑定可变,且消费self后不能再用。 - Go 程序员易踩的坑:忘了
let mut,或into_之后还当借用用。
记忆点:
- 读
&self;改&mut self;移交self。 &mut self⇒ 变量要mut。into_吃掉自己。
Q17. 为什么 for x in &v 里不能 push?
Tags: occasional beginner iterator
适用版本: 全版本
一句话答案:
for x in &v 在整个循环期间持有对 v 的共享借用;push 需要 &mut v 且可能重分配——故 E0502,防的是迭代器内部指针悬垂。
解答:
| |
常见修法:
| |
Go 对比:
| |
- Go 怎么做:
range中append常能编译,语义却容易让人困惑。 - Rust 为什么不同:直接禁止边迭代边扩容同一
Vec。 - Go 程序员易踩的坑:把 Go 里“能跑”的写法搬过来。
记忆点:
&v的 for = 整段循环都在借用。- 先收集 /
iter_mut/ 索引,别边借边push。 - 与 Q9 同一根因。
Q18. 'static 引用和 T: 'static 有什么区别?
Tags: occasional intermediate static
适用版本: 全版本
一句话答案:
&'static T 是「这个引用能活到程序结束」;T: 'static 是「这个类型不含短生命周期借用」(拥有数据的 String、i32、Vec<u8> 都满足)。
解答:
&'static T:引用本身必须指向永不失效的数据(字面量、static 项、Box::leak 等):
| |
T: 'static:约束的是值里有没有“短借”。String 拥有堆数据,满足 T: 'static,但它不是 &'static String:
| |
线程 spawn 要求闭包 T: 'static,因此传拥有的 String 可以,传借用 &s 通常不行——要的是“别把短命借用带进别的线程”,不是要求参数必须是 &'static。
Go 对比:
- Go 怎么做:没有
'static约束;逃逸与 GC 处理跨 goroutine 的指针存活。 - Rust 为什么不同:用
T: 'static在类型层禁止短借出逃。 - Go 程序员易踩的坑:看见
'static就以为必须用全局/字面量;String往往就够。
记忆点:
&'static T= 引用活到进程结束。T: 'static= 类型内无短生命周期借用(拥有值通常 OK)。String: 'static,但&String一般不是&'static String。
Q19. 借用报错时最先该改哪一行?
Tags: advanced diagnostics
适用版本: 1.97.1
一句话答案:
先看编译器标的第一条冲突借用和 note 里的“first borrowed here / mutable borrow later”,优先结束较早的那次借用,而不是盲目 clone 报错行。
解答:
典型 E0502 诊断会指出两处:先发生的共享借用,和后来的可变借用。先改“让第一次借用更早结束”的那一行(缩短使用、提前拷贝、调整顺序):
| |
对照错误形态选策略:
| |
操作顺序建议:① 看清两处借用 ② 缩短第一次借用 / 换序 ③ 换 API(entry、split_at_mut)④ 最后才 clone / RefCell。
Go 对比:
- Go 怎么做:没有这类编译期冲突诊断;问题常拖到运行时。
- Rust 为什么不同:错误信息里已经标出“谁先借、谁后改”。
- Go 程序员易踩的坑:只改最后一行
insert/push,却不结束前面的get/迭代借用。
记忆点:
- 先读 note 里的两处借用点。
- 优先结束较早的借用。
clone是权宜,不是第一步。
Q20. 这一章最实用的判断流程是什么?
Tags: advanced summary borrowing
适用版本: 1.97.1
一句话答案:
只读 → & / &str;要改 → 唯一 &mut;冲突时按「缩作用域 → 换 API → 拷贝值 → 内部可变」升级,不要一上来 clone 或 RefCell。
解答:
日常决策树:
| |
拆冲突清单(与旧章实战清单一致):
| |
错误码速记:E0499 双重 mut;E0502 mut 与共享冲突;E0515 返回局部引用;E0597 活得不够久。
Go 对比:
- Go 怎么做:传指针 + 约定;没有这套编译期流程。
- Rust 为什么不同:流程的每一步都在维持“别名与存活”不变式。
- Go 程序员易踩的坑:把每个借用错误都当成新语言特性,而不是同一棵决策树。
记忆点:
- 能借只读就别可变;能一个
&mut就别两个。 - 冲突:缩域 → API → 拷贝 →
Cell/RefCell。 - 背错误码,对上 Q19 的改法。
Q21. 报错 temporary value dropped while borrowed 是什么意思?
Tags: hot borrowing temporary E0716
适用版本: Rust 1.0+
一句话答案:
你把引用绑到了临时值上,而临时值在当前语句结束就会 drop;引用却还想活到后面——于是 error[E0716]。修法:先用 let 把拥有值存住,再借。
解答:
经典踩法:把临时 String 上的借用塞进要活得更久的容器:
| |
方法链里也会中招:临时值只活到语句末尾,中间产生的借用不能“逃”到下一句去喂给长期容器:
| |
需要拥有修剪后的文本时,转成 String,让所有权离开临时上下文:
| |
本质:借用必须挂在某个活得够久的所有者上;表达式中间的临时值不是合格的长期房东。
Go 对比:
| |
- Go 怎么做:返回的
string自带可用的数据视图,GC 保证安全。 - Rust 为什么不同:
&str不拥有缓冲,临时String一 drop,引用就悬垂。 - Go 程序员易踩的坑:链式
String::from(...).as_str()当普通表达式随便存。
记忆点:
E0716= 临时值死得太早。- 先
let owned = ...;再&owned/.as_str()。 - 要带走文本 →
.to_string()/String::from,别只留引用。
Q22. 为什么“先索引借出来,再 push”编译不过?
Tags: common borrowing Vec push E0502
适用版本: Rust 1.0+
一句话答案:
&v[i] 在元素引用存活期间持有对整个 Vec 的共享借用;push 需要 &mut Vec 且可能重分配——二者冲突,报 E0502。先结束借用(拷贝/克隆出值,或缩作用域),再改容器。
解答:
| |
若后面还要用索引借出的引用,必须先把值拿出来(Copy 直接拷,非 Copy 用 clone):
| |
只读长度再 push 可以,因为临时借用立刻结束(对照 Q9、Q3):
| |
根因不是“下标语法特殊”,而是:活着的元素引用 ≈ 禁止容器再搬家;push 可能搬家。
Go 对比:
| |
- Go 怎么做:允许,但扩容后旧指针可能失效,靠你小心。
- Rust 为什么不同:同一类 bug 在编译期直接拒绝。
- Go 程序员易踩的坑:觉得“我只是读了
v[0]”,不理解借用还绑着整容器。
记忆点:
- 持有
&v[i]时别push/insert。 - 先
copied/cloned/缩作用域,再改。 - 防的是扩容后悬垂引用。