想象一家管理极其严格的私人图书馆。馆里每一本书,在同一时刻只允许借给一位读者。管理员老R捧着一本厚账本,谁借了哪本书、借了多久、何时归还,记得一清二楚。老R从不安排人满馆巡逻,也不搞“定期大扫除”来清点书籍——他靠的是门口张贴的三条铁律:一本书只有一个主人;离馆时书自动回到书架;想看别人的书就办馆内阅览凭证,书不能带走。因为规矩简单、执行严格,这座图书馆几十年从没丢过一本书,从没出现过“两个人都以为书在自己这儿”的糊涂账。 Rust 这门系统编程语言,就是一座这样的图书馆。它没有 Java 那种“保洁阿姨式”的垃圾回收器定时扫描内存,也不像 C++ 那样全靠程序员自觉释放。它用一套 所有权与借用 规则,在代码编译阶段就把“内存归谁、何时释放、谁能借用”全部理清。这篇内容,我们用老R的图书馆当镜子,把 Rust 最核心的概念照得透亮。
对 AI 从业者而言,Rust 所有权是看懂并贡献高性能推理引擎、分布式训练框架的底层钥匙。掌握它,你就能读懂 GitHub 上最火 AI 基础设施代码的内存逻辑。
可视化演示
展示Rust所有权移动语义、不可变与可变借用规则限制、以及编译时借用检查器的验证流程。
本节配有交互动效(进入视口后自动加载,约 3KB)。
所有权移动语义 值从s移动到t后s失效 新所有者t获得值所有权 原所有者s不可再使用
所有权核心概念
走进老R的图书馆,你最先要认识的,是服务台上那本厚厚的账本。每本书被借出时,账本上只登记一位读者的名字——不多不少,恰好一个。这不是管理员小气,而是为了责任明确:书被画脏了、弄丢了,账本上那一个名字就是唯一的责任人。找不到人负责的资产,迟早会变成一笔糊涂账。
Rust把这种“一个值只属于一个变量”的规矩,叫做所有权。当你写出 let book = String::from("你好") 这行代码时,相当于在管理员的账本上填写了一条新记录:book 这个名字,是这块字符串数据的所有者。变量绑定,就是一次“登记入册”。每个值在任一时刻都有一个唯一的所有者,这个所有者拥有对数据完全的读和写权限。
作为所有者,book 拥有完整的权限:它可以读取这份数据,也可以修改它。更重要的是,只有当 book 活着的时候,数据才活着;一旦 book 离开自己所在的作用域(可以理解为一个代码块或函数的范围),Rust 会自动把数据占用的内存“还回书架”——这个过程不需要程序员手动调用 free,也不需要垃圾回收器在某个随机时刻跑来“大扫除”。释放时机的确定性,是 Rust 内存安全的第一层保障。
管理员的账本上永远只有一个名字,这就是 Rust 内存安全的第一块基石。有了它,内存泄漏和双重释放这两类最常见的系统级 Bug,在 Rust 里从一开始就没有生存的土壤。
- 全称量词符号 ∀ 读作“对于所有”,表示“对程序中的每一个数据值 v,都成立后面的性质”。数据值包括字符串、整数、结构体等一切运行时占内存的东西。
- 所有者集合
Owners(v)表示“数据 v 的所有者集合”。集合里装的是所有声称拥有 v 的变量名。Rust 要求这个集合的规模被严格限制。 - 规模为 1竖线包围集合表示“集合的元素个数”。
|Owners(v)| = 1就是说每个数据的所有者数量必须恰好是 1——既不能没人负责,也不能多人同时声称拥有。
所有权三条规则
图书馆门口的铜牌上,用大字刻着三条铁律。老R从不巡视书架的每一个角落,也不设置“内存保洁”岗位,靠的就是这三条规则在读者心中生根发芽:第一,每本书在同一时刻只有一位主人;第二,主人离开图书馆时,书必须自动回到书架;第三,如果你只是想在馆内看看某本书,可以,但必须办“馆内阅览凭证”,凭证上的名字不是主人。
这三条铁律对应到 Rust,就是官方文档白纸黑字的所有权三规则。规则一:Rust 中每一个值都有一个变量作为它的所有者;规则二:当所有者变量离开作用域,这个值会被自动丢弃并释放;规则三:值可以被“借用”给其他代码访问,借用不改变所有权。这三条规则构成了理解 Rust 一切内存行为的总纲。
注意规则二里的“自动”二字——它意味着你不需要写 free 或 delete,也不需要像 Java 那样等 GC 跑到某个不确定的时机。释放内存的动作,在编译阶段就已经被编译器逐行插入到适当的位置。程序运行时,每一块内存的归还时间点都是确定且有序的,不会出现“两个线程同时释放同一块内存”的竞态。
三条规则,一位严谨的管理员。编译器就是老R,它拿着铜牌上的条文,在编译期对每一行代码逐条审查。规则越简单,执行越可靠——这是内存安全最重要的设计哲学。
- 析构函数
Drop(x)表示变量 x 被销毁、它所占的内存被归还的动作。每个 Rust 值在生命周期结束时都会执行这个动作,即使你没写过任何“析构代码”。 - 作用域边界
Scope(x)是 x 的存活范围,通常是它所在的花括号代码块。Exit表示“离开这个范围”的时刻,比如代码执行到右花括号处。 - 当且仅当双箭头符号 ⇔ 表示前后两个命题互为充分必要条件。这意味着“x 离开作用域”是“x 被释放”的唯一触发条件,不依赖运气,不依赖运行时扫描。
转移与移动语义
读者阿A把书转借给了读者阿B。老R面无表情地在账本上把所有者一栏从“阿A”改成“阿B”,然后合上账本。阿A手里空空的,也再没有资格声称这本书在自己名下。如果有人问阿A“你书呢?”,阿A只能耸耸肩:书已经不在我这儿了。
Rust 里的赋值,就是这样一次“转借”。看这段代码:
当你把 s1 赋给 s2,Rust 不会像 C++ 那样做一次“浅拷贝后再让两个指针指向同一块内存”,也不存在“拷贝后两个变量共享所有权”的暧昧状态。它做的是一件更干脆的事:移动。s1 的所有权被整体搬到 s2,而 s1 这个名字从此“失效”,再使用会直接触发编译错误。
转移不只发生在赋值里,还发生在函数传参和返回值中。你把 s 传给一个接收 String 参数的函数,就把这块数据的所有权“送”进了函数;函数返回时你也可以把所有权再“带”出来。理解了这个机制,你就能解释一个常见的 Rust 新手困惑:为什么 fn take(s: String) 调用之后,原变量不能再用了。
账本上名字一改,旧名字立刻作废。正是这种“无回头路”的设计,让 C/C++ 程序员最头疼的野指针、悬垂指针、双重释放问题,在 Rust 的默认路径上根本不可能发生。
- 移动动作
move(x → y)表示所有权从变量 x 转移到变量 y。这一步在语义上把“数据的家”从 x 名下过户给 y。 - 蕴含符号⇒ 表示“如果……那么……”,是逻辑学里的充分条件关系。这里说:只要移动发生了,就一定会导致下面的结果。
- 失效断言
¬Valid(x)读作“x 不再有效”。这不是运行时检查,而是编译器对 x 的静态标记——凡是在移动后还使用 x 的代码,都会被拒绝编译。
let s1 = String::from("你好");let s2 = s1; // 所有权从 s1 转移到 s2// println!("{}", s1); // 编译错误:s1 已被移动!
借用机制
老R的图书馆还有一个宽敞的阅览室。想看书但不想借走?没问题,在服务台办理一张“馆内阅览凭证”,凭这张凭证,你可以在阅览室的座位上翻阅任何一本书,但书绝不离馆。凭证上清清楚楚写着:“持有者:某某;权限:仅限馆内阅读;有效期:不超过图书归还日。”
Rust 的借用和这张凭证完全同构。创建借用的方式,是在变量前加一个与号:let r = &s;。这里的 r 就是指向 s 的一个引用。引用允许你访问数据,但引用绝不是数据的拥有者——所有权还在 s 手里。借用结束时,引用自动失效,数据依旧好端端地归 s 管。
借用为什么重要?因为有时候你只是想看一眼数据、统计一下长度、算一个哈希值,完全没必要把所有权挪来挪去。如果每次都要转移所有权,代码里就会充满“借了还、还了借”的繁琐仪式。借用机制让函数可以“读”数据而不夺走数据,签名也一目了然:fn len(s: &String) -> usize 表示“我只是借用你的字符串,看完就还”。而这一切,由一个叫借用检查器(Borrow Checker)的编译器组件负责监督,它会在编译期翻开账本,逐条核验每一笔借阅有没有超期、有没有越权。
阅览凭证不是借书证,不能把书带出馆。引用也不是所有权,不能代替所有者决定数据何时释放。老R的账本上,每一个数据只有一个正式主人,其余人全是“持证参观”。
- 创建引用
&x是借用语法,读作“对 x 取引用”。就像在服务台办理了一张阅览凭证,凭证的抬头是 x 这块数据。 - 所有者为空
Own(r) = ∅表示借用者 r 的所有权集合为空,即 r 名下没有任何属于它的数据。这是“只看不拿”的合法凭据。 - 蕴含关系⇒ 将“借用”与“不拥有”绑定在一起,暗示编译器在检查借用时,会确认 r 没有越权成为所有者。
可变与不可变引用
阅览室的规矩升级了:图书馆分发两种颜色的阅览券。绿色阅览券(不可变引用 &T)可以同时发给任意数量的读者——十个人坐在同一张长桌前安静地读同一本书,互不干扰;红色批注券(可变引用 &mut T)则只能发给一位读者——因为这位读者要在书页上写写画画,批注期间,其他任何读者都不能再碰这本书,甚至绿券持有者也不行。
Rust 的规则与之一模一样。不可变引用 &T 是只读的,你可以创建多个 &T 同时存在;可变引用 &mut T 是唯一的写权限,同一时刻最多只能有一个 &mut T,而且在它存在期间,任何 &T 都不得出现。翻译成人话就是:要么多个人同时读,要么一个人独占着写,绝不允许读和写同时进行。
这条规则在编译期强制执行,带来的好处是巨大的:它从语言层面消灭了数据竞争。在 C 语言里,两个线程一个读一个写同一个变量,是隐蔽而致命的内存 BUG;在 Rust 里,这种代码根本编译不过,连运行时加锁都省了。程序不需要像 Java 那样依赖 synchronized 关键字小心翼翼地上锁,因为编译器的借用检查器已经把“同时读写的可能性”全部挡在门外。
要么一群人安静地读,要么一个人独占着批注。管理员老R在编译期就把这两种状态隔离开来,绝不给运行时的数据竞争留下任何可乘之机。
- 存在不可变引用
∃ &x表示“存在一个指向 x 的不可变引用”。多个&x同时存在是合法的,但一旦要考虑下面的情况,事情就变味了。 - 存在可变引用
∃ &mut x表示“存在一个指向 x 的可变引用”。它拥有写权限,因而要求独占访问。 - 推出矛盾⊥ 是逻辑学中的“矛盾”记号。整个式子说明:当读和写同时出现在同一份数据上时,借用检查器判定会产生数据竞争,于是拒绝编译——这就是“水火不容”的数学表达。
Copy与Clone
图书馆配备了两台处理书籍的设备。复印机(Copy)放在服务台旁边,一按按钮,“咔”地一声,立刻吐出一份和原件一模一样的复印件,适合纸张轻薄的书页;手抄台(Clone)则要人工抄写,适合需要完整深拷贝的厚书,你必须在单子上签字申请,费用不菲,但抄出来的内容和原件完全独立。
Rust 中的 Copy 和 Clone,就是复印机与手抄台的关系。Copy 是一个 trait(可以理解为一种“能力标签”),实现了 Copy 的类型,在赋值或传参时会自动做一份位拷贝。整数、浮点数、布尔值、字符这些存在栈上的简单类型都是 Copy 的,所以 let b = a; 之后 a 还能继续用——因为 b 拿到的是 a 的完整复印件,两者互不干扰。而 String、Vec 这些把数据存在堆上的类型,并没有 Copy 这一能力标签,赋值只能触发 move。
那想把 String 复制一份怎么办?调用它的 Clone 方法:let b = a.clone();。这会显式地对堆上的数据做一次深度拷贝,原数据和副本各自独立。注意“显式”二字:它需要你主动写出来,因为深拷贝有开销,Rust 不希望你在无意间制造一大堆隐藏的数据副本。还有一个重要的约束:任何实现了 Drop(自定义析构)的类型,都不能再实现 Copy,否则就会破坏 Copy“无副作用复制”的语义契约。
复印机打出来的复印件,和原件天各一方、互不相欠;手抄台抄出来的厚书,也绝不会和原书共享任何一页纸。Copy 与 Clone 的区分,让“复制数据”这件事在 Rust 里变得可预期、可控制。
- 发生复制
Copy(x)表示数据 x 执行了一次隐式位拷贝。这个拷贝由编译器自动插入,不需要写任何代码。 - 原值有效
Valid(x)表示复制之后原来的 x 依然可以使用。这正是 Copy 与 move 最大的区别——move 让原变量失效,Copy 让原变量继续存活。 - 副本有效x' 读作“x 撇”,表示 x 的副本。逻辑与符号 ∧ 连接了两个条件,意思是原件和副本必须同时有效,缺一不可。
悬垂引用防护
老R的阅览室里,发生过这么一起“未遂事件”:一位读者把书还回了书架,手里却还攥着阅览凭证,想继续翻看已经归位的书。管理员在一个读者走出阅览室时拦住了他:“书已经放回书架了,你的凭证失效了。”老R之所以能精准拦截,靠的不是现场盯梢,而是每张凭证上都印着有效期:凭证的有效期,绝不越过书的归还日。
在 C/C++ 世界里,这种“数据已释放、引用还在用”的错误叫悬垂引用(dangling reference)。它是最难排查的内存错误之一,通常会在程序运行的某个随机时刻崩溃——今天没问题,明天崩了,还复现不出来。Rust 的解决方案不是在运行时报错,而是让编译器在编译期就进行生命周期检查:每个引用都有一个生命周期,记录它“活着”的时间区间;每个数据也有一段被所有权的存活区间。借用检查器会验证:数据的存活时间必须覆盖引用的使用时间。
举个例子,下面的代码在 Rust 中根本无法编译:
编译器会明确告诉你:"x does not live long enough"(x 活得不够久)。一眼看过去,这条错误信息像是管理员在说:你的阅览凭证有效期超过了书的归还日,这不行。
账本把书的归还日写得明明白白,阅览凭证的有效期也休想越过归还日。Rust 用同样的精神,把“悬垂引用”这个 C 语言的噩梦,提前摁死在编译失败的警告里。
- 生命周期
Lifetime(x)表示数据 x 从创建到释放所跨越的时间区间。所有权的规则决定了这个区间的起点和终点。 - 引用使用期
Usage(r)表示引用 r 从创建到最后一次被使用的时长。引用只在它被使用的这些时刻才真正“需要”数据活着。 - 不小于⪰ 符号表示“在时间长度上不小于”。借用检查器的核心工作,就是逐一核对每个引用的这两段时间,确保前者覆盖后者,才能签发“安全凭证”。
let r;{ let x = 5; r = &x;} // x 在这里被释放println!("{}", r); // 编译错误:x 活得不够久
AI 应用场景
高性能 AI 推理服务
阶段:模型部署与推理优化
解决的问题:AI 服务要处理每秒上千次推理请求,传统 C++ 容易因内存错误崩溃,Python 则因 GC 停顿导致延迟抖动。Rust 的所有权模型在编译期就消除了内存泄漏和数据竞争,推理进程可以稳定运行数周不重启,就像书架上每一本书都能准确找到自己的位置。
具体案例:某 OCR 团队用 Rust 重写了推理网关,把每个请求的图片解码、特征提取、文字识别串在一条无 GC 的流水线上。重构后单机吞吐从 800 QPS 提升到 2100 QPS,P99 延迟从 120ms 降到 45ms,且连续运行三个月零内存错误报警。
深度学习框架的自定义算子
阶段:训练框架开发
解决的问题:张量运算涉及大量内存读写,多线程并行时若两个算子同时访存,会产生数据竞争,导致训练 loss 忽高忽低。借用规则让共享权重使用不可变引用、参数更新使用唯一可变引用,竞争在编译期就被拦截,如同老R在阅览室门口就把绿券红券的冲突挡下。
具体案例:某团队用 Rust 为 PyTorch 编写了一个 flash-attention 风格的注意力算子。在多 GPU 训练场景下,借助 &T 实现权重共享、&mut T 实现梯度累加,既没有锁开销,也从未出现过一次“随机 NaN”事故。自研算子的调参时间从三天缩短到三小时。
嵌入式 AI 设备固件
阶段:边缘计算部署
解决的问题:智能传感器的内存只有几十 KB,跑不了 Java 虚拟机,也很难承受垃圾回收的随机停顿。Rust 的所有权在编译期确定每块内存的释放时机,让设备固件的内存占用始终平整如镜,就像馆内每一本书都按时归位,从不需要临时大扫除。
具体案例:一款语音唤醒芯片的固件改用 Rust 编写,模型推理的中间张量通过所有权链精确复用内存,最终将运行时内存从 46KB 压到 21KB;因为没有任何 GC 停顿,麦克风采样与推理的实时性也大幅改善,唤醒延迟稳定在 80ms 以内。
Python 演示
class RustOwnershipSim: """用 Python 模拟 Rust 的所有权与借用规则""" def __init__(self): self.owner_map = {} # 数据id -> 当前所有者 self.value_map = {} # 数据id -> 数据内容 self.read_set = {} # 数据id -> 不可变借用者集合 self.write_map = {} # 数据id -> 可变借用者 def bind(self, var_name, value): if var_name in self.owner_map.values(): raise RuntimeError(f"{var_name} 已被绑定为其他数据的所有者") data_id = id(value) self.owner_map[data_id] = var_name self.value_map[data_id] = value self.read_set.setdefault(data_id, set()) self.write_map.setdefault(data_id, None) return data_id def move_value(self, src, dst): data_id = self._find_by_owner(src) if data_id is None: raise RuntimeError(f"{src} 已被移动,无法再使用") if self.write_map.get(data_id) is not None: raise RuntimeError(f"{src} 正被可变借用,不能转移") self.owner_map[data_id] = dst def borrow_read(self, var_name): data_id = self._find_by_owner(var_name) if data_id is None: raise RuntimeError(f"借用失败:{var_name} 已被移动") if self.write_map.get(data_id) is not None: raise RuntimeError(f"借用失败:{var_name} 正被可变借用") self.read_set[data_id].add(var_name) def borrow_write(self, var_name): data_id = self._find_by_owner(var_name) if data_id is None: raise RuntimeError(f"借用失败:{var_name} 已被移动") if self.write_map.get(data_id) is not None: raise RuntimeError(f"借用失败:{var_name} 已有可变借用") if self.read_set.get(data_id): raise RuntimeError(f"借用失败:{var_name} 存在不可变借用") self.write_map[data_id] = var_name def drop_owner(self, var_name): data_id = self._find_by_owner(var_name) if data_id is None: return del self.owner_map[data_id] del self.value_map[data_id] print(f"自动释放:数据 {data_id} 已归还内存") def _find_by_owner(self, var_name): for k, v in self.owner_map.items(): if v == var_name: return k return None def main(): sim = RustOwnershipSim() sim.bind("s1", [1, 2, 3, 4]) sim.move_value("s1", "s2") try: sim.borrow_read("s1") except RuntimeError as e: print("编译器拦截:", e) sim.borrow_read("s2") print("不可变借用 s2 成功") try: sim.borrow_write("s2") except RuntimeError as e: print("编译器拦截:", e) sim.drop_owner("s2") sim.bind("num", 7) print("Copy 模拟:num 是整数,赋值即拷贝") num2 = 7 print("num 仍然可用:", num2 == 7) if __name__ == "__main__": main()
self.owner_map[data_id] = var_name模拟“账本登记”:记录每个数据属于哪个变量,这是所有权的核心台账。raise RuntimeError(f"{src} 已被移动,无法再使用")模拟编译器对“移动后使用”的拦截,被移动的变量一旦被引用就立即报错。if self.write_map.get(data_id) is not None:可变借用检查:如果数据已被某变量可变借用,新的借用一律拒绝,这正是“读写互斥”的体现。del self.owner_map[data_id]模拟离开作用域时的自动释放,对应 Rust 中的 Drop 行为,无需手动清理。sim.borrow_read("s2")` 后 `sim.borrow_write("s2")演示先读后写的失败场景:不可变借用存在时,可变借用申请会被拒绝。
常见误区
误区我听说 Rust 是内存安全的语言,那它肯定像 Java 一样有个 GC 线程在后台定期扫描内存、自动回收没用的对象吧?
正解Rust 没有垃圾回收器。它靠所有权和借用规则在编译期静态决定每一块内存的释放时机,运行时零 GC 开销,也不会出现“回收引起的停顿”。你甚至可以在写嵌入式实时系统时精确预测内存行为,这在 Java 世界是不可想象的。就像老R靠账本管理藏书,而不是靠保洁大队每晚大扫除。
为何常见:市面上大多数内存安全语言(Java、Go、Python)都依赖 GC,人们很容易把“内存安全”和“垃圾回收”画上等号。
误区我写了 `let mut x = 5;`,这应该就是创建了一个可变引用吧?那为什么还要写 `&mut x` 呢?
正解let mut x 只是说变量 x 本身可以被重新赋值,比如 x = 6,这是绑定可变性,不涉及任何借用。&mut x 才是创建指向 x 数据的可变引用,才会进入借用检查器的“唯一写权限”审查。前者随时可以,后者受到严格排他规则约束。这就像“你有这本书的长期借阅权”和“你临时借到了这本书”完全是两回事。
为何常见:两个语法都含有 mut,初学者极易把“可变绑定”和“可变借用”两个不同概念混为一谈。
误区Clone 能复制数据,Copy 也能复制数据,那它们应该是一回事吧?以后我随便用哪个都行?
正解Copy 是编译器隐式触发的位拷贝,赋值或传参时自动发生,且原值仍然有效,只适用于栈上的简单类型;Clone 是显式调用 .clone() 方法,可以做深度拷贝,开销明显更大,适用于堆类型。而且类型一旦实现 Drop,就禁止再实现 Copy,二者的语义约束完全不同。复印机一按就吐出复印件,手抄台却需要你逐页申请抄写,两台机器的使用规则天差地别。
为何常见:从结果看,两者都能得到“两份数据”,初学者容易忽略触发时机和前提条件的巨大差异。
误区Rust 这也不能做、那也不能做,写什么都报错,所有权规则纯粹是束缚,让写代码的效率变得特别低。
正解所有权只是把“谁负责释放内存”说清楚而已。配合借用(&)、可变借用(&mut)和智能指针(Rc/Arc),绝大多数编程模式都能优雅表达。适应规则之后,你省掉的是调试内存 Bug 的大量时间——很多 C++ 项目三分之一的时间都花在排查内存错误上,Rust 把这部分直接清零了。老R的规章不是限制你看书,而是让你每次借书都清清楚楚、不丢不坏。
为何常见:学习初期被编译错误连续打击,容易把保护机制误认为障碍,其实那是编译器在替你守住安全底线。
误区悬垂引用应该和 Java 的空指针一样,程序运行到访问它的那一行、那一瞬间才抛出异常吧?
正解Rust 的借用检查器在编译期就会拦截悬垂引用。代码压根编译不过,也就永远没有机会在运行时出错。编译器会告诉你“x 活得不够久”,而不是等到线上崩了才由运维报警。老R在你还书时就已经在账本上划掉了你的凭证,绝不会让你拿着失效的凭证走出阅览室。
为何常见:其他语言(C/C++)的悬垂指针确实是运行时才暴露,人们容易误以为“运行时崩溃”是所有语言处理这个问题的唯一方式。
面试题
Q1. Rust 中每个值在同一时刻最多可以有几个所有者?
- A.0 个
- B.1 个← 正确答案
- C.2 个
- D.无限个
答案:B。Rust 的核心铁律是“每个值有且仅有一个所有者”。0 个会导致值无人负责,2 个或无限个会导致双重释放和所有权混乱。就像图书馆的账本上每本书只写一个读者名字,多写一个就分不清谁该还书了。
Q2. 执行 let a = String::from("hi"); let b = a; 之后,变量 a 处于什么状态?
- A.a 仍可正常读取
- B.a 已被移动而失效← 正确答案
- C.a 和 b 共享数据
- D.a 被自动释放为空
答案:B。String 不是 Copy 类型,赋值 let b = a 会触发所有权移动。移动后,a 在编译期被标记为失效,任何对 a 的使用都会被拒绝。就像书转借给 b 后,a 名下已经没有这本书了。
Q3. 下列哪种类型在 Rust 中实现了 Copy trait(赋值后原变量仍可用)?
- A.String 字符串
- B.Vec 容器类型
- C.i32 整数类型← 正确答案
- D.Box 智能指针
答案:C。存储在栈上的简单类型如 i32、f64、bool、char 都实现了 Copy,赋值自动拷贝、原值依然有效。而 String、Vec、Box 等涉及堆内存的类型没有 Copy,赋值会移动所有权。这就像复印机只能印单页纸,厚厚的手抄书必须走手抄台。
Q4. 以下哪些属于 Rust 所有权的三条基本规则?
- A.每个值只有一位所有者← 正确答案
- B.所有者离开作用域时自动释放← 正确答案
- C.值可通过引用被临时借用← 正确答案
- D.所有变量必须声明为 mut
答案:ABC。Rust 所有权三规则即:唯一所有者、离开作用域自动释放、可通过引用临时访问(借用)。D 是错误的,变量是否需要 mut 取决于你是否要修改它,并非强制。馆门口的三条铜牌没有“必须声明 mut”这条。
Q5. 下列关于借用(引用)的说法,哪一项是正确的?
- A.借用会将所有权转移给借用者
- B.借用者的存活时间可以长于所有者
- C.借用不会转移数据所有权← 正确答案
- D.借用之后原变量立即失效
答案:C。借用只是创建指向数据的引用,并不转移所有权,原变量在借用结束后依然可用。借用者的存活时间不能长于所有者,否则会产生悬垂引用,编译器会拒绝。阅览证不改变借书证上的名字,书的主人始终不变。
Q6. 在 Rust 中,关于不可变引用与可变引用的规则,正确的有?
- A.不可变引用可同时多个← 正确答案
- B.可变引用可同时多个
- C.有不可变引用禁可变引用← 正确答案
- D.有可变引用时可多不可变
答案:AC。不可变引用(&T)允许多个并存,因为只读不冲突;而可变引用(&mut T)具有唯一性和排他性,它存在时不可变引用也不能出现,这是防止数据竞争的编译期铁律。红色批注券一发出,绿色阅览券全部作废,反之亦然。
Q7. Rust 为什么不需要垃圾回收器也能保证内存安全?
- A.因为 Rust 程序从不分配堆内存
- B.因为所有权规则让编译器静态确定释放时机← 正确答案
- C.因为 Rust 运行时定期扫描回收内存
- D.因为 Rust 语言禁止使用任何指针类型
答案:B。Rust 借助所有权和借用规则,在编译期就确定了每个值的释放时机(离开作用域即 Drop),运行时无需 GC 扫描,也没有回收停顿。A、C、D 均不符合事实:Rust 可以分配堆内存、没有运行时 GC、也允许使用引用和指针。这就像老R靠账本而不是靠保洁大队管理图书馆。
Q8. 执行 let mut s = String::from("x"); let r1 = &s; let r2 = &mut s; 这行代码会怎样?
- A.能通过编译,两者可以共存
- B.不能通过编译,二者互斥不可共存← 正确答案
- C.能通过编译,但必须删除 mut
- D.不能通过编译,因为 s 未声明 mut
答案:B。代码中先创建了不可变引用 r1,随后又试图创建可变引用 r2。借用规则规定:当存在不可变引用时,不能再创建可变引用,因此编译失败。这与 s 是否 mut 声明无关,C 和 D 都不对。一位读者正拿着绿券在看,管理员绝不会让红券持有人同时出现在书旁。
Q9. 关于 Copy 和 Clone 的说法,哪一项是错误的?
- A.Copy 是隐式触发的位拷贝
- B.实现 Drop 的类型不能再实现 Copy
- C.Clone 可以自定义深拷贝逻辑
- D.Copy 适用于一切类型数据复制← 正确答案
答案:D。Copy 只适用于栈上可安全位拷贝的类型(整数、浮点、布尔等),并非一切类型。带有堆内存的类型若伪装成 Copy,会在释放时引发双重释放,因此 Rust 禁止堆类型实现 Copy,它们只能用显式的 Clone 深拷贝。复印机不能复印厚书,厚书只能走手抄台。
Q10. 关于 Rust 对悬垂引用的防护机制,下列说法正确的有?
- A.借用检查器能在编译期拦截悬垂引用← 正确答案
- B.生命周期分析要求数据活得比引用更久← 正确答案
- C.悬垂引用要等到运行时才会暴露问题
- D.所有生命周期都必须程序员手动标注
答案:AB。借用检查器在编译期通过生命周期分析,确保数据的生命周期不短于引用的使用期(A、B 正确)。因此悬垂引用在 Rust 中根本无法通过编译,不会拖到运行时才崩溃(C 错误)。生命周期标注多由编译器自动推断,只有复杂场景才需要手动标注,D 说法过于绝对。凭证有效期不越过归还日,这是老R早就印在账本上的规矩。