笃行
首页
个人 & 心法
互联网/硬件后台
游戏基础架构
UE 引擎
游戏业务
AI / 大模型
数据结构与算法
机器学习数学
通用基础
GitHub
首页
个人 & 心法
互联网/硬件后台
游戏基础架构
UE 引擎
游戏业务
AI / 大模型
数据结构与算法
机器学习数学
通用基础
GitHub
  • 通用后台基础(跨域)

    • 并发模型 · 进程 / 线程 / 协程
    • 操作系统核心与零拷贝
    • Go 语言基础与常见陷阱
    • C++11 语言基础与常见陷阱
    • C++20 语言基础与常见陷阱
    • Rust 语言基础与常见陷阱
    • 设计模型 · Actor / CSP / Reactor / 同步异步
    • GC 与 STW · Go / JVM
    • 可观测性
    • 时序异常检测(EWMA / ARIMA / 滑动窗口)
    • 数据库范式与存储引擎:从关系型到向量库
    • MySQL InnoDB 索引与事务
    • Redis 版本演进 & 分布式
    • 消息队列 · 可靠投递与选型
    • 分布式事务 · 2PC / TCC / Saga / 最终一致性
    • HTTP / HTTPS / TLS 与 RPC
    • 加密基础:对称 / 非对称 / 哈希与组合模式
    • 叙事主骨架轴选择方法论 SOP

Rust 语言基础与常见陷阱

所有权与移动 · 借用规则与生命周期 · String vs &str · Rc/Arc/RefCell 内部可变性 · 循环引用泄漏 · ? 与错误处理 · Send/Sync · async 与 .await · trait 对象 · panic vs Result

一句话抓手

Rust 的陷阱几乎都源自三个真相:所有权唯一——赋值/传参默认是「移动」而非拷贝,move 后原变量失效;借用规则铁律——任一时刻要么多个共享引用 &T、要么唯一可变引用 &mut T,二者不可并存(编译期强制);生命周期只是给编译器证明「引用不会比被引用者活得久」的标注,不改变运行时行为。抓住这三条,八成 borrow checker 报错当场看懂。

场景问题

打个比方(所有权与借用):一份数据就像一本书,只有一个主人(所有权唯一)。借用规则则是图书馆的借阅铁律:要么很多人同时"只读"地传阅(多个共享引用 &T),要么某一个人把书借走"涂改"、期间谁都不许碰(唯一可变引用 &mut T)——绝不允许一边有人在改、一边有人在读(否则读到改一半的脏内容)。而 move 就是把书的所有权直接过户给别人,原主人从此两手空空(原变量失效)。类比失效边界:这套铁律是 borrow checker 在编译期就掐死的静态规则;但 RefCell 能把检查推迟到运行时——编译照过,可一旦运行时真的同时 borrow_mut 两次,就当场 panic。所以"内部可变性"不是取消了规则,只是把"什么时候查"从编译期挪到了运行期,赌的是你自己不会违规。

面试与实战高频「为什么编译不过 / 会 panic」题,本质在考对所有权模型的理解:

题目考点直觉答案往往错在
let b = a; 之后用 a 报错移动语义非 Copy 类型赋值是 move,a 已失效
遍历 Vec 时 push 报错借用冲突迭代持有 &,push 需 &mut,不可并存
Rc 互相持有会泄漏吗循环引用会,Rust 不防内存泄漏(泄漏是安全的)
RefCell 借用两次可变运行时借用检查编译过,运行时 borrow_mut 冲突直接 panic
函数返回局部变量引用生命周期编译不过,引用比被引用者活得久
String 能当 &str 用吗Deref 强制转换能,&String 自动 deref 成 &str
unwrap() 生产环境用吗panic vs ResultNone/Err 上 unwrap 直接 panic 崩进程
多线程共享 RcSend/SyncRc 非线程安全,编译期就拒绝,用 Arc

实现方案

所有权与移动

每个值有唯一所有者,所有者离开作用域时值被 drop(析构)。非 Copy 类型的赋值、传参、返回都是移动:所有权转移,原变量失效。

let a = String::from("hi");
let b = a;              // 移动:a 的所有权给了 b
// println!("{}", a);  // ❌ 编译错误:value borrowed after move

Copy 类型(整数、bool、char、以及全由 Copy 组成的元组/结构)赋值是按位拷贝,原变量仍可用。Clone 是显式深拷贝(a.clone())。

借用规则(编译期强制)

任一作用域内,对同一数据:要么有任意多个不可变引用 &T,要么有且仅有一个可变引用 &mut T——绝不同时存在。这就是「共享不可变、可变不共享」,从根上消灭数据竞争。

迭代中修改容器

let mut v = vec![1, 2, 3];
for x in &v {           // 不可变借用 v
    v.push(*x);         // ❌ 需要可变借用 v,与上面的 &v 冲突
}

规避:先收集要改的,循环外再改;或改用索引 for i in 0..v.len();或用 retain/drain 等专门 API。

NLL(Non-Lexical Lifetimes)让借用在最后一次使用后即结束,而非到作用域尾,所以很多「看起来冲突」的代码其实能过。

生命周期标注

生命周期 'a 是给编译器的证明,标注引用之间的存活关系,不产生任何运行时代码:

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { // 返回值活得不超过 x、y
    if x.len() > y.len() { x } else { y }
}

大多数场景由生命周期省略规则自动推导,无需手写。返回局部变量的引用必然编译不过(悬垂),要么返回所有权(String),要么让调用方传入缓冲。

String vs &str

  • String:拥有的、堆分配、可增长的 UTF-8 字符串
  • &str:借用的字符串切片(指针 + 长度),指向别处的 UTF-8 数据;字符串字面量是 &'static str

&String 通过 Deref 强制转换自动变 &str,所以函数参数优先写 &str(更通用,String 和字面量都能传)。注意:字符串按 UTF-8 存储,s[0] 这样的字节索引不被允许(会切坏多字节字符),要用 .chars() / .bytes()。

内部可变性:Rc / Arc / RefCell / Cell

  • Rc<T>:单线程引用计数共享所有权(非原子,非 Send)
  • Arc<T>:原子引用计数,跨线程共享
  • RefCell<T>:把借用检查推迟到运行时,允许在只有 &self 时改内部(borrow / borrow_mut)
  • Cell<T>:整值 get/set 的内部可变,无引用

常见组合 Rc<RefCell<T>>(单线程共享可变)、Arc<Mutex<T>>(多线程共享可变)。

RefCell 运行时 panic + Rc 循环泄漏

let c = RefCell::new(5);
let b1 = c.borrow_mut();
let b2 = c.borrow_mut(); // ❌ 运行时 panic: already borrowed

RefCell 把借用冲突从「编译错误」变成「运行时 panic」——灵活性换来的是崩溃风险。

Rc 互相强引用会内存泄漏(计数永不归零)——Rust 保证内存安全,但不保证不泄漏(泄漏不是 unsafe)。规避:一个方向用 Weak<T>(Rc::downgrade,upgrade() 拿回 Option<Rc>)打破环。

错误处理:Result / Option / ? / panic

  • 可恢复错误用 Result<T, E>,缺值用 Option<T>
  • ? 运算符:Ok/Some 时取值,Err/None 时提前返回(自动 From 转换错误类型)
  • panic! / unwrap() / expect():不可恢复,展开栈或直接 abort

unwrap 是生产事故高发点

unwrap() / expect() 在 None / Err 上直接 panic。库代码和服务里滥用 = 一个边界输入崩整个线程/进程。生产代码用 ? 传播、match/if let 处理、或 unwrap_or/unwrap_or_else 给默认值。unwrap 只留给「逻辑上不可能失败」且你愿意为其崩溃负责的地方。

并发:Send / Sync

  • Send:类型的所有权可跨线程转移
  • Sync:&T 可跨线程共享(即 T 可被多线程同时引用)

这两个 auto trait 由编译器自动推导,把线程安全变成类型系统层面的编译期检查:Rc 非 Send(计数非原子),跨线程传会编译报错,逼你换 Arc。「无畏并发」正来自此——数据竞争在编译期就被拒。

async / .await

async fn 返回一个惰性的 Future,不 .await(或不交给 executor)就完全不执行。Rust 只提供语言层,运行时(tokio / async-std)需自选。

async 的两个高频坑

  • 忘记 .await:let f = do_async(); 什么都没发生,f 只是个未轮询的 Future(编译器通常有 must_use 警告)。
  • 跨 .await 持有非 Send 类型(如 RefCell 的 borrow、MutexGuard):在多线程 executor 上会编译报错或死锁;.await 点前释放守卫。

trait 对象与泛型

  • 泛型 <T: Trait>:静态分发,单态化,零运行时开销,代码膨胀
  • dyn Trait(Box<dyn Trait>、&dyn Trait):动态分发,虚表,有运行时开销但代码紧凑、可异构集合

为什么这么做

  • 所有权 + 借用检查:Rust 的核心命题是「无 GC 的内存安全 + 无数据竞争的并发」。把「谁负责释放」「谁能改」编码进类型系统,编译期证明,运行时零开销——既有 C++ 的性能又有内存安全,代价是编译期与 borrow checker 搏斗。
  • 生命周期:让编译器能证明所有引用都不悬垂,且不需要运行时检查或 GC。标注只是把程序员脑中的存活假设写给编译器验证。
  • Result + ?:错误是值、必须显式处理(#[must_use]),杜绝「忘记检查返回码」;? 让传播不啰嗦。区分 panic(bug/不可恢复)与 Result(预期内错误)让错误处理意图清晰。
  • Send/Sync:把线程安全下沉到类型系统,编译期挡住数据竞争,这是「无畏并发」的技术基础。

为什么别的选择不行

  • 为什么不用 GC:GC 有停顿和内存开销,且无法确定性释放非内存资源。所有权在编译期确定释放点,零运行时成本,适合系统级/嵌入式/高性能场景——这是 Rust 存在的理由。
  • 为什么不允许共享可变引用:可变别名(aliasing + mutation)正是数据竞争和迭代器失效的根源。禁止它换来的是编译期消灭一大类 bug;需要共享可变时显式用 RefCell/Mutex,把风险局部化并标注出来。
  • 为什么 Rust 不防内存泄漏:泄漏不违反内存安全(不会读写非法内存),且完全防住会限制表达力(Rc 环有合法用途)。所以 mem::forget、Rc 环泄漏都是 safe 的——安全 ≠ 无泄漏。
  • 为什么 async 是惰性的:惰性 Future 让运行时能自由调度、组合、取消,且零成本(不 poll 就不占资源)。代价是必须搭配 executor 且容易「忘记 await」。

沉淀结论

面试速答清单:

  • 所有权唯一;非 Copy 类型赋值/传参是 move,move 后原变量失效
  • 借用铁律:多个 &T 或唯一 &mut T,二者不可并存;NLL 让借用在末次使用后即结束
  • 生命周期是编译期证明,零运行时开销;不能返回局部变量的引用
  • 参数优先 &str(&String 自动 deref);字符串是 UTF-8,不能字节索引
  • RefCell 把借用检查移到运行时,冲突 panic;Rc 环内存泄漏(安全但泄漏),用 Weak 打破
  • Rc<RefCell<T>> 单线程共享可变;Arc<Mutex<T>> 多线程
  • 错误用 Result/Option + ? 传播;unwrap/expect 会 panic,生产慎用
  • Send/Sync 编译期保证线程安全;Rc 非 Send,跨线程用 Arc
  • async fn 返回惰性 Future,不 .await 不执行;别跨 await 持有非 Send 守卫
  • 泛型 = 静态分发零开销;dyn Trait = 动态分发有虚表;Rust 内存安全 ≠ 不泄漏

记忆口诀

  • 所有权:唯一 / move 后失效 / Copy 才拷贝
  • 借用:共享不可变 &T / 可变不共享 &mut T / NLL 末次即结束
  • 共享可变:单线程 Rc<RefCell> / 多线程 Arc<Mutex> / 环用 Weak 打破
  • 错误:Result+? 传播 / unwrap 会 panic / Send·Sync 编译期查
  • async:Future 惰性 / 不 await 不跑 / 别跨 await 持非 Send 守卫

内容来源

关键点整理自 The Rust Programming Language(官方 Book)、Rustonomicon、Rust Reference 与社区共识(NLL、Send/Sync 规则)重写为五段式。请以官方文档为准。

自测:合上资料能说清楚吗?

  1. 为什么 let b = a; 之后再用 a 会编译报错?什么类型不会?
参考答案

非 Copy 类型赋值是 move(所有权转移),a 失效。整数/bool/char 等 Copy 类型是按位拷贝,原变量仍可用;Clone 是显式深拷贝。

  1. 借用规则的「铁律」是什么?NLL 又放宽了什么?
参考答案

同一数据要么多个 &T、要么唯一 &mut T,不可并存(共享不可变、可变不共享)。NLL 让借用在最后一次使用后即结束(而非到作用域尾),故很多看似冲突的代码能过。

  1. RefCell 和编译期借用检查有何不同?借用冲突时会怎样?
参考答案

RefCell 把借用检查推迟到运行时,允许 &self 下改内部。冲突(如两次 borrow_mut)不是编译错误而是运行时 panic——用灵活性换崩溃风险。

  1. 对比 Rc 与 Arc、Rc<RefCell<T>> 与 Arc<Mutex<T>>,各用在什么场景?
参考答案

Rc 非原子计数、非 Send,仅单线程;Arc 原子计数、可跨线程。共享可变时:单线程用 Rc<RefCell<T>>,多线程用 Arc<Mutex<T>>。跨线程传 Rc 编译期即被拒。

  1. async fn 返回什么?为什么说它「惰性」,有哪两个高频坑?
参考答案

返回惰性 Future,不 .await(或不交 executor)就不执行。坑一:忘记 await,什么都不发生;坑二:跨 await 持有非 Send 守卫(RefCell borrow、MutexGuard)在多线程 executor 上报错或死锁。

最近更新: 2026/9/10 11:38
Prev
C++20 语言基础与常见陷阱
Next
设计模型 · Actor / CSP / Reactor / 同步异步