Rust API设计:内部可变性使用抉择——&self还是&mut self?
我拥有C++开发背景,对Rust的内部可变性(interior mutability)存在困惑。以下是我针对该主题的研究代码。
我认同,从借用检查器的角度来看,处理那些内部状态可能随时变更的结构体的多个引用是难以实现的,而这正是内部可变性能够发挥作用的场景。此外,在《Rust编程语言》第15.5章“RefCell与内部可变性模式”中,Messenger trait及其MockMessenger结构体实现的例子让我思考:即使明确需要某种可变性,系统地优先使用&self而非&mut self是否是常见的API设计方式?
Messenger的实现发送消息时怎么可能不修改内部状态?唯一的例外是仅打印消息,这与&self的使用一致,但一般情况可能需要写入内部流,这涉及缓冲、更新错误标志等操作——所有这些显然都需要&mut self,例如File的Write trait实现。依赖内部可变性解决这个问题,在我看来就像在C中使用const_cast或滥用mutable成员,仅仅因为应用其他部分在const正确性上不一致(这是C学习者的常见错误)。
回到我下方的示例代码,我面临两个选择:
- 将change_e()到change_i()改为使用&mut self(编译器不会报错,尽管并非必须),以与修改存储整数状态的事实保持一致性;
- 继续使用&self,因为内部可变性允许这样做,即使实际上修改了存储整数的状态。
这个决定不仅影响结构体本身,还会对使用该结构体的应用程序的表达能力产生重大影响。第二种方案显然更实用,因为仅涉及共享引用,但这符合Rust的预期规范吗?我在Rust API指南中找不到答案,是否有类似C++CoreGuidelines的其他Rust文档?
研究代码
/* $ rustc int_mut.rs && ./int_mut initial: 1 2 3 4 5 6 7 8 9 change_a: 11 2 3 4 5 6 7 8 9 change_b: 11 22 3 4 5 6 7 8 9 change_c: 11 22 33 4 5 6 7 8 9 change_d: 11 22 33 44 5 6 7 8 9 change_e: 11 22 33 44 55 6 7 8 9 change_f: 11 22 33 44 55 66 7 8 9 change_g: 11 22 33 44 55 66 77 8 9 change_h: 11 22 33 44 55 66 77 88 9 change_i: 11 22 33 44 55 66 77 88 99 */ struct Thing { a: i32, b: std::boxed::Box<i32>, c: std::rc::Rc<i32>, d: std::sync::Arc<i32>, e: std::sync::Mutex<i32>, f: std::sync::RwLock<i32>, g: std::cell::UnsafeCell<i32>, h: std::cell::Cell<i32>, i: std::cell::RefCell<i32>, } impl Thing { fn new() -> Self { Self { a: 1, b: std::boxed::Box::new(2), c: std::rc::Rc::new(3), d: std::sync::Arc::new(4), e: std::sync::Mutex::new(5), f: std::sync::RwLock::new(6), g: std::cell::UnsafeCell::new(7), h: std::cell::Cell::new(8), i: std::cell::RefCell::new(9), } } fn show(&self) -> String // &足够(只读) { format!( "{:3} {:3} {:3} {:3} {:3} {:3} {:3} {:3} {:3}", self.a, self.b, self.c, self.d, self.e.lock().unwrap(), self.f.read().unwrap(), unsafe { *self.g.get() }, self.h.get(), self.i.borrow(), ) } fn change_a(&mut self) // &mut是必需的 { let target = &mut self.a; *target += 10; } fn change_b(&mut self) // &mut是必需的 { let target = self.b.as_mut(); *target += 20; } fn change_c(&mut self) // &mut是必需的 { let target = std::rc::Rc::get_mut(&mut self.c).unwrap(); *target += 30; } fn change_d(&mut self) // &mut是必需的 { let target = std::sync::Arc::get_mut(&mut self.d).unwrap(); *target += 40; } fn change_e(&self) // !!! 此处无&mut !!! { // 在C++中,保护独立整数(e)的std::mutex会作为结构体的两个数据成员。 // 由于我们的目的是修改整数(e),且std::mutex::lock()并非const(但可通过mutable关键字隐藏), // 该成员函数在C++中不会是const。但在Rust中,即使实际修改了结构体的内部状态(受保护的整数), // 仍允许使用&self(等效于const成员函数)。 let mut target = self.e.lock().unwrap(); *target += 50; } fn change_f(&self) // !!! 此处无&mut !!! { // 实际修改整数(与e类似) let mut target = self.f.write().unwrap(); *target += 60; } fn change_g(&self) // !!! 此处无&mut !!! { // 实际修改整数(与e、f类似) let target = self.g.get(); unsafe { *target += 70 }; } fn change_h(&self) // !!! 此处无&mut !!! { // 实际修改整数(与e、f、g类似) self.h.set(self.h.get() + 80); } fn change_i(&self) // !!! 此处无&mut !!! { // 实际修改整数(与e、f、g、h类似) let mut target = self.i.borrow_mut(); *target += 90; } } fn main() { let mut t = Thing::new(); println!(" initial: {}", t.show()); t.change_a(); println!("change_a: {}", t.show()); t.change_b(); println!("change_b: {}", t.show()); t.change_c(); println!("change_c: {}", t.show()); t.change_d(); println!("change_d: {}", t.show()); t.change_e(); println!("change_e: {}", t.show()); t.change_f(); println!("change_f: {}", t.show()); t.change_g(); println!("change_g: {}", t.show()); t.change_h(); println!("change_h: {}", t.show()); t.change_i(); println!("change_i: {}", t.show()); }
作为从C转Rust的开发者,我完全理解你对内部可变性的困惑——毕竟这和C的const模型逻辑差异很大,甚至会让你觉得内部可变性像C++里的mutable或const_cast,但其实二者有本质区别。咱们一步步拆解你的问题:
1. 内部可变性≠绕过规则,而是场景化的安全设计
首先要明确:Rust的内部可变性(比如RefCell、Mutex、Cell)不是用来绕过借用检查器的“后门”,而是为了解决特定场景下的合法需求——比如当你需要在共享引用(&self)下修改内部状态,但同时能保证安全(要么通过运行时检查,要么通过线程同步机制)。
对比C++:
- C++的
mutable是绕过静态const检查,编译器不会帮你验证修改的安全性,全靠开发者自己保证; - Rust的内部可变性是在语言规则内提供的安全机制:比如
RefCell会在运行时检查借用规则,违反时直接panic;Mutex通过互斥锁保证线程安全。这和C++里滥用mutable或const_cast有本质区别。
2. 为什么Messenger例子优先用&self?区分逻辑可变性与物理可变性
你提到的《Rust编程语言》里的Messenger例子,核心是区分逻辑可变性和物理可变性:
- Rust的
&self表示的是逻辑上的不可变——即从API调用者的角度,这个操作不会改变对象的“逻辑状态”,调用者不需要关心内部的实现细节; &mut self表示逻辑上的可变——调用者明确知道这个操作会改变对象的外部可见状态。
比如Messenger的send_message:调用者只需要“发送消息”这个行为,不需要知道Messenger内部是否维护了消息计数、缓冲队列这些状态。这些内部修改是物理上的,但对调用者来说是透明的,属于实现细节,所以用&self是合理的。
而你提到的File的Write trait,写入文件会直接改变外部可见的状态(文件内容),这属于逻辑上的可变,所以必须用&mut self——调用者明确知道这个操作会改变对象的状态。
3. 你的Thing结构体:选择&self还是&mut self?
回到你的代码,核心判断标准是:这些修改操作是否属于“逻辑上的可变”?
从你的show方法可以看到,change_e()到change_i()的修改会直接反映在输出中——也就是说,调用这些方法后,Thing的外部可见状态明显改变了。这种情况下:
- 优先选择&mut self更符合Rust的API设计规范:因为你明确告诉调用者“这个操作会改变Thing的状态”,借用检查器可以帮你管理这些可变操作,避免潜在的数据竞争或非法借用问题。虽然编译器允许你用&self,但这会隐藏可变性,让调用者难以推理代码的行为。
- 如果坚持用&self:只有当这些修改是完全内部的、对调用者透明的场景才合理——比如内部缓存的更新、统计计数的累加,而这些状态不会通过
show或其他方法暴露给外部。但你的代码显然不符合这个场景。
4. Rust的API规范参考:类似C++ Core Guidelines的文档
你提到的类似C++ Core Guidelines的Rust文档是Rust API Guidelines,里面有几个相关原则:
- 最小化可变性:只在必要时暴露可变性,避免不必要的
&mut self; - 清晰表达意图:API的签名应该准确反映操作的本质——如果操作会改变逻辑状态,就用
&mut self;如果只是内部实现的修改,用&self配合内部可变性是允许的,但要谨慎。
5. 对你代码的具体分析
看你代码里的各个字段:
a-d:修改这些字段会直接改变外部可见状态,用&mut self是完全正确的,符合Rust的借用规则;e-i:虽然内部可变性允许用&self,但由于修改会通过show暴露给外部,属于逻辑上的可变操作,所以更适合用&mut self——这样API的意图更清晰,也能让借用检查器发挥作用。
总结
内部可变性是Rust解决特定场景问题的工具,不是用来替代&mut self的。选择&self还是&mut self的核心是:
- 如果操作会改变对象的逻辑可见状态,用
&mut self; - 如果只是内部实现细节的修改,且能保证安全,用
&self配合内部可变性。
这样既符合Rust的设计哲学,也能让代码更易读、更安全。
内容的提问来源于stack exchange,提问作者prog-fh

