如何在不使用 mut 的前提下“修改”一个值?

解读

面试官抛出此题,并非考察语法奇技淫巧,而是验证候选人对 Rust「所有权与可变性分离」这一核心哲学的理解深度。
国内一线厂(华为、阿里、字节、PingCAP)的 Rust 岗面试中,该题出现频率极高,通常作为**“内存安全”环节的分水岭**题目:

  • 若只答“用 Cell/RefCell”,会被追问“是否违背零成本抽象”“运行时 panic 场景如何兜底”;
  • 若完全答不上来,则直接判定“未脱离 C/C++ 可变引用思维”,一票否决

关键得分点:在不产生 &mut 的前提下,通过安全抽象把“可变”从语言层面转移到运行时或硬件层面,同时不破坏 Send/Sync 语义

知识点

  1. 内部可变性(Interior Mutability)
    Rust 把“可变”拆成两层:

    • 语言层可变mut 绑定 / &mut 引用)
    • 内部可变(通过 unsafe 封装,对外仍表现为不可变)
      标准库提供三种零成本或带成本的封装:
    • Cell<T>:要求 T 实现 Copy无运行时开销,单线程内绕开借用检查。
    • RefCell<T>:运行时借用计数,单线程,违反规则触发 panic,非零成本
    • UnsafeCell<T>:内部可变性之源,所有底层都基于它,但裸用 unsafe 需自负其责。
  2. 原子指令(AtomicXXX)
    多线程场景下,不使用 mut 也能修改的本质是借助 CPU 提供的原子指令(CAS、Swap、FetchAdd)。Rust 通过 AtomicUsize/AtomicPtr 等封装,对外接口全是 &self编译期不生成 &mut,却能在并发环境安全修改。

  3. 持久化数据结构(Persistent Data Structure)
    纯函数式思路:不原地修改,而是返回新值+结构共享。例如 im::Vectorpush 返回新 Vector,旧绑定仍不可变;虽语义上“修改”,但物理内存无写操作完全避开 mut

  4. 内存映射(Mmap)与硬件自修改
    内核或嵌入式场景,把页设为 WX 或 RW,通过 *const 指针写闪存/寄存器;语言层面无 mut实际硬件层面已修改。国内做操作系统(如华为 openEuler Rust 驱动)会考察此边界。

答案

标准答案应分层给出,体现“场景→机制→代价”

  1. 单线程、Copy 类型

    use std::cell::Cell;
    let v = Cell::new(5);
    v.set(10);        // 无需 mut,零成本
    
  2. 单线程、非 Copy 类型

    use std::cell::RefCell;
    let r = RefCell::new(vec![1, 2]);
    r.borrow_mut().push(3);  // 运行时检查,panic 风险
    
  3. 多线程、整数/ptr 修改

    use std::sync::atomic::{AtomicUsize, Ordering};
    let a = AtomicUsize::new(0);
    a.fetch_add(1, Ordering::Relaxed); // 无 mut,CPU 原子指令
    
  4. 零开销“纯函数式”写法

    let old = Rc::new(vec![1]);
    let new = Rc::new((*old).clone()); // 返回新值,旧值不变
    
  5. unsafe 终极手段(仅内核/嵌入式答)

    use std::cell::UnsafeCell;
    let u = UnsafeCell::new(42);
    unsafe { *u.get() = 99; } // 语言层无 mut,硬件层已写
    

面试话术模板
“在不使用 mut 的场景下,我会先判断线程模型与性能预算。单线程 Copy 类型优先 Cell零成本且符合 Rust 零开销抽象;非 Copy 用 RefCell 但会加单元测试覆盖 panic 路径;多线程则转 Atomic无锁队列,保证 Send/Sync;若做操作系统或嵌入式,UnsafeCell + 原子指令是最后兜底,需配合 Miri 与 KASAN 做内存模型验证。”

拓展思考

  1. 为什么 Cell 要求 Copy?
    若允许 Move 语义,set 会把旧值 Drop,可能触发析构函数内部对同一 Cell 的二次借用,造成悬垂指针;Copy 保证位复制无析构,从而编译期杜绝 UAF

  2. RefCell 的 panic 能否被捕获?
    可以,但一旦 panic 触发,借用标记位仍处于“已借”状态,后续任何 borrow 都会直接 panic,整个对象永久失效;国内生产代码(如蚂蚁区块链 VM)会用 scopeguard 在 panic 时强制清零标记,但需 unsafe必须封装成内部 crate 并过 CR 评审

  3. Atomic 的 Ordering 选型在国内大厂如何落地?
    华为云高并发网关实践:

    • 计数器用 Relaxed无需同步
    • 状态机用 Acquire/Release保证 happens-before
    • 单生产者单消费者用 fence + Relaxed 替代 SeqCst实测 QPS 提升 8%
  4. 纯函数式结构共享的内存阈值?
    字节跳动 WASM 沙盒项目中,结构共享深度超过 7 层时,RC 开销反超复制;内部规范规定:小于 256 字节且只读引用计数 < 7 才用持久化数据结构,否则原地 Copy + mut 更省内存。

  5. 面试反向提问技巧
    答完后主动追问:“贵司的 Rust 代码基里,哪些场景必须绕开 mut?是否允许 unsafe 内部可变性?”
    既展示对工程落地的关注,也把面试官引入自己熟悉的技术细节战场提升面试主导权