想象大学摄影社团的器材室,柜子里躺着一台社团唯一的单反相机。它不是某个成员的私人物品,而属于整个社团。谁要用,都要先借;用完之后,相机必须回到器材室。更特别的是,器材室有两类权限:一类是隔着展示柜看,可以很多人同时看;另一类是打开改装室拿相机换镜头、调参数,这时整个器材室只会借出一把钥匙,一次只能有一个人进去。这就是 Rust 借用检查器(Borrow Checker)的缩影:它在编译期检查“谁在什么时候用什么东西”,确保不会出现两个人在同一时间动手改同一台相机,也不会有人拿着已经还回去的相机继续拍照。 Rust 没有垃圾回收器,却能做到内存安全,靠的正是这套“器材室管理员”。管理员会在你写代码时就盯住每一笔借阅记录:共享借用可以同时存在很多笔,但可变借用必须唯一;借用结束的时间,不是等到你回到社团,而是你最后一次按下快门的那一刻。后者就是 NLL(Non-Lexical Lifetimes,非词法生命周期)的核心思想。这篇文章会用这个“相机器材室”的故事,带你看懂借用检查器的内部规则、常见报错,以及如何写出让编译器点头的优雅代码。

岗位关联

对 AI 从业者而言,理解借用检查器意味着你能读懂 Rust 高性能推理服务、数据管道和工具链的编译错误,写出并发安全、内存高效的 AI 基础设施代码。

目录
  1. 可视化演示
  2. 所有权基石
  3. 借用规则
  4. 可变借用唯一
  5. 借用检查机制
  6. NLL 生命周期
  7. 报错信息解读
  8. 生命周期标注
  9. 编译器演进
  10. AI 应用场景
  11. Python 演示
  12. 常见误区
  13. 面试题

可视化演示

本节配有交互动效(进入视口后自动加载,约 3KB)。

借用规则——可变借用唯一性 多个&T: 只读共享OK 1个&mut: 独占修改OK &T+&mut共存:编译错误 要么N个&T,要么1个&mut,不可兼得

所有权基石

器材室里的相机不是某个人的私有物,而是社团资产。站在 Rust 的世界里,这个“社团”就是变量,相机就是它保存的值。每个值在任意时刻都只有一个主人,这个主人负责在不需要时把它释放掉。

当相机被正式移交给另一个社团时,原来的社团不能再拿它去拍照。这种“换主人”的行为叫移动(Move)。但一些轻量的小物品,比如一枚存储卡上的编号,复制一份也毫无风险,这种类型叫 Copy。两者的本质区别是:移动后旧绑定失效,复制后旧绑定仍然可用。

回到器材室:把相机正式转让给隔壁社团之后,原社团的登记册上就不再允许任何人从旧账本里借相机了。这种“旧主人失效”的规则,是 Rust 防止重复释放和悬垂指针的第一道防线。

after_move(v)={old_usablev:Copyold_invalidv:Move\displaystyle\mathrm{after\_move}(v)=\begin{cases}\mathrm{old\_usable} & v:\mathrm{Copy}\\\mathrm{old\_invalid} & v:\mathrm{Move}\end{cases}
这个公式说的是:一个值经过“转移”之后,如果它是 Copy 类型,原来的变量还能继续用;如果它是 Move 类型,原来的变量就失效了。
  1. after_move(v)表示变量 v 经历一次所有权转移之后的状态。
    after_move(v)\mathrm{after\_move}(v)
  2. Copy 分支如果 v 是整数、布尔值这类 Copy 类型,转移时实际上做了拷贝,旧绑定依然有效。
    v:Copy⇒old_usablev:\mathrm{Copy} \Rightarrow \mathrm{old\_usable}
  3. Move 分支如果 v 是 String 这类非 Copy 类型,转移会真正移动资源,旧绑定从此不可再用。
    v:Move⇒old_invalidv:\mathrm{Move} \Rightarrow \mathrm{old\_invalid}

借用规则

社团成员想看相机,不一定非要拿走相机。器材室有两种权限:隔着展示柜看,多人同时看没问题;进改装室换镜头,一次只能借出一把钥匙。Rust 把这两种权限对应为共享借用 &T 和可变借用 &mut T。

共享借用允许同时存在多个,因为它们都只读,不会破坏数据。可变借用允许修改数据,所以必须独占,否则两个修改者会互相踩脚。更严格地说,只要有一个可变借用存在,任何共享借用都不能同时存在。

展示柜前可以围十个同学同时看,但改装室的门永远只有一把钥匙。如果你在共享阅读时突然想拿钥匙去改相机,管理员会立刻拦住你:要么先让所有人看完,要么等你改完再让大家看。

\displaystylen_{\text{mut}} \in \{0,1\},\quadn_{\text{shared}} \in \mathbb{N},\quadn_{\text{mut}} \times n_{\text{shared}} = 0
这个公式把借用规则压缩成一行:可变借用数量只能是 0 或 1;共享借用数量可以是任意自然数;两者的乘积必须是 0,意味着它们不能同时出现。
  1. n_mut当前作用域中可变借用的个数,编译器只允许 0 或 1。
    nmut∈{0,1}n_{\text{mut}} \in \{0,1\}
  2. n_shared共享借用的个数,可以是 0 个、1 个或多个。
    nshared∈Nn_{\text{shared}} \in \mathbb{N}
  3. 乘积为 0只要有一个可变借用,共享借用就只能是 0;共享借用一旦存在,可变借用就必须是 0。
    nmut×nshared=0n_{\text{mut}} \times n_{\text{shared}} = 0

可变借用唯一

改装室的门锁决定了一个铁律:可变借用必须唯一。为什么这么严格?因为两个同时写同一块数据,或者一个读一个写同一块数据,都会产生数据竞争(Data Race)。数据竞争轻则得到错误结果,重则让程序崩溃甚至产生安全漏洞。

借用检查器会沿着程序的控制流,检查每一个时间点上的借用状态。它不关心运行时那一刻有没有真的发生冲突,而是在编译期就证明:任何时刻的可变借用集合里最多只有一个元素。

有了这把唯一钥匙,就相当于器材室保证:任何瞬间只有一个人能碰到相机的内部设置。于是“两个人同时调光圈”和“一个人调光圈、另一个人看取景器”这两种危险局面,在写代码的阶段就被消灭了。

∀t,∣{r∣is_mut(r)∧active(r,t)}∣≤1\displaystyle\forall t,\quad\left|\{ r \mid \mathrm{is\_mut}(r) \land \mathrm{active}(r,t) \}\right| \le 1
这个公式表达的是:对于任意时刻 t,所有处于活跃状态且是可变借用的引用 r,它们组成的集合大小不能超过 1。
  1. forall t对所有可能的程序执行时刻做检查,而不是只检查某一行。
    ∀t\forall t
  2. 集合构造收集所有满足条件的引用 r,条件是“它是可变借用”并且“在 t 时刻活跃”。
    {r∣is_mut(r)∧active(r,t)}\{ r \mid \mathrm{is\_mut}(r) \land \mathrm{active}(r,t) \}
  3. 基数不超过 1这个集合的大小只能是 0 或 1,也就是可变借用在同一时刻最多只有一个。
    ∣{⋯ }∣≤1\left|\{ \cdots \}\right| \le 1

借用检查机制

管理员怎么发现冲突?他不是等出事后翻监控,而是在借出时就拿台账划线。每条借用记录都有借出时间和归还时间,如果两条记录的时间段重叠,并且其中一条是可变借用,管理员就会打一个红叉。

借用检查器做的事情完全一样,只不过它分析的是程序控制流图(CFG,Control Flow Graph)。它先计算每个引用从哪一行诞生、在哪一行最后一次被使用,然后检查访问路径是否重叠。如果两个访问操作访问了同一块内存区域,并且至少有一个是写操作,就判定为冲突。

这台账不是运行时才记账的,而是在编译阶段由编译器一笔一画写完。正因为如此,Rust 不需要垃圾回收器在运行时扫描内存,也能在编译期给你一个“内存安全”的承诺。

conflict(a,b)=overlap(a,b)∧(write(a)∨write(b))\displaystyle\mathrm{conflict}(a,b)=\mathrm{overlap}(a,b) \land (\mathrm{write}(a) \lor \mathrm{write}(b))
这个公式给出了冲突的精确条件:两个访问操作 a 和 b 访问的区域有重叠,并且至少其中一个是写操作,才会构成借用冲突。
  1. conflict(a,b)判定访问操作 a 和 b 是否构成借用冲突。
    conflict(a,b)\mathrm{conflict}(a,b)
  2. overlap(a,b)两个访问路径指向的内存区域是否有交集。
    overlap(a,b)\mathrm{overlap}(a,b)
  3. write(a) or write(b)两个操作中只要有一个是写操作,就可能破坏数据一致性。
    write(a)∨write(b)\mathrm{write}(a) \lor \mathrm{write}(b)
  4. 合取关系重叠和写操作必须同时成立,两个只读操作即使重叠也不冲突。
    overlap(a,b)∧⋯\mathrm{overlap}(a,b) \land \cdots

NLL 生命周期

旧版借用检查器有个很傻的毛病:它认为借用会活到所在代码块结束,也就是遇到 } 才算归还。这就好比器材室规定:你最后一次按快门之后,相机仍然算被你占着,直到你回到社团大楼。于是明明你已经不需要相机了,别人也不能借。

NLL(Non-Lexical Lifetimes,非词法生命周期)改变了这个规则:借用的结束点不是词法作用域的右花括号,而是该引用最后一次被使用的位置。看这个例子:

在旧规则下,preview 会活到整个块结束,导致第三行无法创建可变借用;在 NLL 规则下,preview 在 println! 之后就结束了,所以第二段借用合法。

NLL 让器材室记住了“最后一次按快门”这个更聪明的还相机时间。于是,读完相机信息后把相机交给下一个人改装,不再需要把它带回社团大楼,代码也更符合人类的直觉。

τ(r)=[born(r),  last_use(r)]\displaystyle\tau(r)=[\mathrm{born}(r),\;\mathrm{last\_use}(r)]
这个公式说明:一个借用 r 的生命周期区间,从它诞生的程序点开始,到它最后一次被使用的程序点结束,而不是到所在的代码块结束。
  1. tau(r)借用 r 的存活区间。
    τ(r)\tau(r)
  2. born(r)借用诞生的位置,也就是创建引用的那一行。
    born(r)\mathrm{born}(r)
  3. last_use(r)借用最后一次被使用的位置,之后它就不再参与任何计算。
    last_use(r)\mathrm{last\_use}(r)
  4. 闭区间生命周期是一个连续区间,从出生点到最后使用点,中间每一步都算活跃。
    [born(r),  last_use(r)][\mathrm{born}(r),\;\mathrm{last\_use}(r)]
rust4 行
let mut camera = String::from("社团相机");let preview = &camera;println!("{}", preview);      // preview 的最后一次使用在这里let edit = &mut camera;        // OK:preview 已经"死"了

报错信息解读

当你违反借用规则时,管理员不会只写“你错了”,他会在借条上划出冲突的线段,并给一个错误码:E0502 表示你尝试在共享借用还活着时创建可变借用;E0499 表示你试图同时持有两个可变借用;E0505 表示你在一个借用仍然活跃时移动了原值。

理解错误码的关键是顺着控制流找“最后一次使用”。很多时候,只要把某个变量的使用范围缩小,比如把最后一句 println! 提前,代码就能通过。借用检查器给出的错误码不是随机代号,而是对冲突类型的一种分类。

看到这些错误码时,不妨想象管理员在台账上画了一个红圈。你要做的不是绕开管理员,而是让借阅时间段错开:把最后一次使用提前,或者把移动操作放到借用结束之后。

error={E0502shared∩mut≠∅E0499∣mut_borrows∣>1E0505move∩live_borrow≠∅\displaystyle\mathrm{error}=\begin{cases}E0502 & \mathrm{shared} \cap \mathrm{mut} \neq \varnothing\\E0499 & |\mathrm{mut\_borrows}| > 1\\E0505 & \mathrm{move} \cap \mathrm{live\_borrow} \neq \varnothing\end{cases}
这个公式把三类常见错误归纳为三种集合冲突:共享借用和可变借用同时存在、可变借用数量超过一个、移动操作撞上了仍然存活的借用。
  1. E0502 分支共享借用集合与可变借用集合有交集,也就是读写重叠。
    E0502:shared∩mut≠∅E0502: \mathrm{shared} \cap \mathrm{mut} \neq \varnothing
  2. E0499 分支可变借用的数量大于 1,打破了唯一性。
    E0499:∣mut_borrows∣>1E0499: |\mathrm{mut\_borrows}| > 1
  3. E0505 分支移动操作与某个存活中的借用发生了碰撞。
    E0505:move∩live_borrow≠∅E0505: \mathrm{move} \cap \mathrm{live\_borrow} \neq \varnothing

生命周期标注

有些器材需要跨社团借用,比如一个函数接收一台相机的引用,又要返回这台相机的某个镜头数据。这时光有借用类型还不够,管理员需要在借条上写明“这两个引用之间有什么关系”。Rust 用生命周期参数 'a 来表达这种关系。

生命周期参数不是真正的运行时时间,而是编译期的一个名字,用来连接多个引用的存活区间。只要输入引用和输出引用标上同一个 'a,编译器就知道:返回的引用不可能比输入借用活得更久。大多数时候,Rust 的省略规则能自动推断生命周期,所以你很少需要手写。

借条上写同一个代号,管理员就知道两台设备要一起归还。生命周期参数本身不创造安全,它只是让“谁活多久”这种关系变得更加清晰可见。

fn f<′a>(x:&′aT)→&′aU\displaystyle\mathrm{fn}~f<'a>(x: \&'a T) \to \&'a U
这个公式展示了一个带生命周期标注的函数签名:输入引用 x 和输出引用都被同一个生命周期 'a 约束,编译器因此知道它们共享同一段存活区间。
  1. <'a>声明一个生命周期参数,相当于借条上写一个代号。
    <′a><'a>
  2. &'a T输入引用 x 的类型,它的生命周期是 'a。
    &′aT\&'a T
  3. -> &'a U输出引用也被标注为 'a,表示它和输入引用绑定在同一段存活时间内。
    →&′aU\to \&'a U
  4. 整体关系生命周期标注描述的是引用之间的约束,而不是数据本身的大小或位置。
    fn f<′a>(x:&′aT)→&′aU\mathrm{fn}~f<'a>(x: \&'a T) \to \&'a U

编译器演进

器材室的规则不是一天变成现在这样的。早期规则很保守:借用必须活到代码块结束,这导致很多逻辑上安全的代码被编译器拒绝。后来 Rust 团队用数据流分析改进了规则,让借用结束在最后一次使用点,这就是 NLL。

从 Rust 2018 起,NLL 逐步成为默认行为,极大减少了“编译器过于严格”的抱怨。未来实验中的 Polonius 模型还会继续细化借用关系,让检查结果更精确。每一次演进,本质上都是让“归还时间”的判定更接近人的直觉。

从“必须回到社团才算还”到“最后一次用就算还”,器材室的管理哲学越来越聪明。借用检查器的演进,也让 Rust 在保留内存安全的同时,不再用一堆无谓的报错折磨开发者。

τlex(r)⊇τNLL(r)⊇τPolonius(r)\displaystyle\tau_{\text{lex}}(r) \supseteq \tau_{\text{NLL}}(r) \supseteq \tau_{\text{Polonius}}(r)
这个公式表示三种生命周期模型的关系:词法生命周期区间最大,NLL 区间更小,未来的 Polonius 模型理论上更精确。
  1. tau_lex(r)旧规则下借用 r 的生命周期,一直延伸到整个词法块结束。
    τlex(r)\tau_{\text{lex}}(r)
  2. tau_NLL(r)NLL 规则下的生命周期,只到最后一次使用点。
    τNLL(r)\tau_{\text{NLL}}(r)
  3. tau_Polonius(r)Polonius 模型进一步收缩无效区间,处理更多边界情况。
    τPolonius(r)\tau_{\text{Polonius}}(r)
  4. 包含关系左边包含右边,代表生命周期判定越来越精确,检查器允许的合法程序越来越多。
    τlex(r)⊇τNLL(r)⊇τPolonius(r)\tau_{\text{lex}}(r) \supseteq \tau_{\text{NLL}}(r) \supseteq \tau_{\text{Polonius}}(r)

AI 应用场景

高并发模型推理服务

阶段:模型部署与在线更新

解决的问题:在推理服务中,多个请求同时读取模型权重,而配置更新线程需要修改权重。传统做法用锁保护,容易死锁或漏锁;Rust 借用检查器在编译期就保证“读取时可以多路并行,写入时必须独占”,把并发风险消灭在运行之前。

具体案例:一个基于 Rust 的 ONNX 推理服务,每隔一段时间重新加载增量模型权重。加载线程持有 &mut 更新权重,推理线程持有 & 读取权重,编译器通过借用区间自动判断:只要旧引用最后一次读取结束,新权重就能安全替换。

数据预处理与张量缓冲区复用

阶段:特征工程与数据预处理

解决的问题:处理海量样本时,反复分配内存会拖慢训练管线。更高效的做法是复用一块张量缓冲区:先用不可变借用读原始数据,再用可变借用写入归一化结果。借用检查器能确保写缓冲区时不会有人同时读同一段内存。

具体案例:一个 Rust 实现的特征工程管道,先读取 CSV 的行切片,再把这些切片写入预先分配的浮点数组。NLL 让“读原始行”的借用在一轮计算结束后立刻释放,下一轮马上复用缓冲区,整个过程零额外拷贝。

AI 工具链中的静态分析

阶段:AI 辅助编程与代码分析

解决的问题:用 Rust 编写 linter、格式化器或代码生成插件时,需要同时遍历 AST 节点并修改缓存。借用规则让只读遍历和可变缓存的交替变得清晰,防止分析工具在遍历过程中意外篡改数据结构。

具体案例:一个 Rust 实现的代码补全插件,将代码库解析成 AST 后,先用共享借用遍历语法树,等遍历结束再拿可变借用更新索引缓存。NLL 让这两种借用可以出现在同一个函数体里,而不必拆成两个大阶段。

Python 演示

python41 行
# Simulate Rust borrow checker: NLL vs lexical lifetime. class Borrow:    def __init__(self, kind, born, last_use):        self.kind = kind        self.born = born        self.last_use = last_use  class BorrowChecker:    def __init__(self):        self.borrow_list = []           # all borrow records     def add_borrow(self, kind, born, last_use):        self.borrow_list.append(Borrow(kind, born, last_use))     def _overlaps(self, left, right):        return left.born <= right.last_use and right.born <= left.last_use     def has_conflict(self):        for i, left in enumerate(self.borrow_list):            for right in self.borrow_list[i + 1:]:                if self._overlaps(left, right) and (left.kind == "mut" or right.kind == "mut"):                    return True        return False  def simulate():    nll = BorrowChecker()    nll.add_borrow("shared", 0, 1)   # preview: last use at t = 1    nll.add_borrow("mut", 2, 3)      # edit: born at t = 2    print("NLL conflict:", nll.has_conflict())     lexical = BorrowChecker()    lexical.add_borrow("shared", 0, 5)  # old rule: alive until block end    lexical.add_borrow("mut", 2, 3)    print("Lexical conflict:", lexical.has_conflict())  if __name__ == "__main__":    simulate()
目的:用 Python 模拟借用检查器的冲突判定,直观展示 NLL 与旧词法规则的区别。
  1. class Borrow:定义一条借用记录,包含类型、诞生时间和最后使用时间。
  2. self.borrow_list = []借用检查器保存所有借阅记录的台账。
  3. def _overlaps(self, left, right):判断两条借用的生命周期区间是否相交。
  4. return left.born <= right.last_use and right.born <= left.last_use两个区间相交的充要条件是左边开始不晚于右边结束,且右边开始不晚于左边结束。
  5. if self._overlaps(left, right) and (left.kind == "mut" or right.kind == "mut"):重叠且至少一条是可变借用时,才构成冲突。
  6. nll.add_borrow("shared", 0, 1)按 NLL 语义,共享借用最后一次使用在 1,到 2 时已经结束,所以不冲突。
  7. lexical.add_borrow("shared", 0, 5)旧规则把生命周期拖到块结束 5,于是在 2 到 3 之间与可变借用重叠,编译器报错。

常见误区

误区我一开始以为只要创建了引用,原变量就要被“冻结”,再也不能读也不能写,直到借用结束。

正解共享借用只是让大家隔着展示柜看相机,原变量本身也能正常读取。只有可变借用存在时,原变量才不能同时被使用,因为可变借用的语义是“唯一钥匙”。Rust 真正禁止的是“写和读重叠”以及“两个写重叠”。

为何常见:因为看到 E0502 报错里既有共享借用又有可变借用,很容易误以为所有借用都会锁死原变量。

误区有人把 NLL 理解成一种“聪明的运行时回收机制”,好像程序跑着跑着,某个引用就会被自动清理。

正解NLL 完全是编译期的数据流分析,它不产生运行时开销。编译器算出某个借用的最后一次使用点,然后只在那一点之前认为借用存活。这就像管理员在借条上提前写好了归还时刻,而不是雇一个保洁阿姨到点来打扫。

为何常见:因为“生命周期”这个词听起来很像引用计数或垃圾回收,容易让人联想到运行时行为。

误区初学时遇到借用错误,第一反应是“那我多写几个生命周期标注,编译器是不是就不报错了?”

正解生命周期标注只是告诉编译器“两个引用之间的关系”,它不能改变两个可变借用真的在时间上重叠的事实。哪怕借条写得再详细,也不能让两个人同时拿同一把钥匙。

为何常见:看到函数签名里出现 'a 'b 会觉得很神秘,以为它是让代码通过编译的魔法开关。

误区觉得只要给引用标上 `'static`,它就能绕过借用检查,像“永久权限”一样安全。

正解'static 表示数据存活于整个程序运行期间,它确实很长,但并不意味着可以随便把借来的数据标成 'static。如果数据本来只活在某个函数里,硬标 'static 只会让代码无法通过编译,或者逼你把数据泄漏到全局区。

为何常见:看到字符串字面量有 'static 生命周期,就误以为它是一个万能强力注解。

面试题

Q1. 关于 Rust 借用规则,下列哪项是正确的?

单选题 · easy

  • A.多个共享借用不能同时存在
  • B.可变借用同时只能存在一个← 正确答案
  • C.共享借用和可变借用可并存
  • D.借用必须在使用后显式归还

答案:B。可变借用唯一是借用检查器的核心规则。共享借用可以同时有多个,所以 A 错;共享借用和可变借用不能并存,所以 C 错;借用由编译器自动管理,不需要显式归还,所以 D 错。

Q2. 下面哪种操作会触发移动语义?

单选题 · easy

  • A.将 String 变量赋值给另一个变量← 正确答案
  • B.调用 String 的 len 方法
  • C.使用 i32 整数更新自身值
  • D.对 String 进行不可变借用

答案:A。String 是非 Copy 类型,赋值给新变量会发生移动,旧变量失效。len 方法只是共享借用,整数是 Copy 类型不会移动,不可变借用更不会移动。

Q3. NLL 的主要改进是什么?

单选题 · mid

  • A.把借用检查从编译期移到运行期
  • B.让生命周期在使用点结束而非块尾← 正确答案
  • C.允许多个可变借用同时存在
  • D.自动为所有变量添加 'static

答案:B。NLL 的核心是借用结束于最后一次使用点,而不是词法块结束。它仍是编译期检查,也不会放宽可变借用唯一性。

Q4. E0502 错误通常表示什么?

单选题 · mid

  • A.变量在共享借用期间被移动
  • B.可变借用与共享借用区间重叠← 正确答案
  • C.函数返回值缺少生命周期标注
  • D.程序在运行阶段才发生异常

答案:B。E0502 表示在共享借用存活时试图创建可变借用,读写区间发生重叠。移动和生命周期标注不足是其他错误码。

Q5. 借用检查器在哪个阶段工作?

单选题 · easy

  • A.Rust 程序运行的每个时刻
  • B.编译时对源码做的静态分析← 正确答案
  • C.链接阶段对二进制动态检查
  • D.测试用例执行阶段的检查

答案:B。借用检查是编译器的一部分,在编译期对源码做静态分析,不产生运行时开销。

Q6. 生命周期标注 'a 的主要作用是什么?

单选题 · hard

  • A.告诉编译器变量占用多大内存
  • B.表达多个引用之间的存活关系← 正确答案
  • C.让引用在堆上分配内存
  • D.改变借用检查器的检查顺序

答案:B。'a 是编译期用来连接多个引用存活期的名字。它不描述内存大小,也不决定分配位置。

Q7. 哪些情况会被 Borrow Checker 拒绝?

多选题 · mid

  • A.一个可变借用与共享借用重叠← 正确答案
  • B.两个可变借用区间重叠← 正确答案
  • C.可变借用结束后再次借用
  • D.共享借用期间读取原变量

答案:AB。可变借用与共享借用重叠、两个可变借用重叠都会破坏唯一性或读写隔离,所以被拒绝。可变借用结束后再次借用是合法的;共享借用期间读取原变量也是合法的。

Q8. NLL 让下列哪些代码可以通过检查?

多选题 · mid

  • A.借用最后一次使用后再移动原变量← 正确答案
  • B.借用结束后创建新的可变借用← 正确答案
  • C.同一作用域两个可变引用并存
  • D.共享借用存活时修改原变量

答案:AB。NLL 让借用结束于最后一次使用点,因此移动原变量和创建新的可变借用都可以提前发生。两个可变引用并存和共享借用时修改仍然违反规则。

Q9. 关于 'static 生命周期,下列说法正确的是?

单选题 · hard

  • A.任何引用都应标注为 'static
  • B.'static 表示数据存活于整个程序期间← 正确答案
  • C.'static 能绕过借用检查约束
  • D.只有字符串字面量才拥有 'static

答案:B。'static 表示数据在整个程序运行期间都有效。它不是万能注解,也不能绕过借用检查,字符串字面量只是 'static 的常见例子之一。

Q10. 关于借用检查与内存安全的关系,正确的有?

多选题 · hard

  • A.借用检查在编译期防止悬垂指针← 正确答案
  • B.NLL 依赖运行时的引用计数
  • C.可变借用唯一性可防止数据竞争← 正确答案
  • D.移动语义可避免重复释放内存← 正确答案

答案:ACD。借用检查确保引用不越过数据存活期,从而防止悬垂指针;可变借用唯一性防止数据竞争;移动语义让旧绑定失效,避免同一块内存被释放两次。NLL 是编译期静态分析,不依赖运行时引用计数。