You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Rust API设计:内部可变性使用抉择——&self还是&mut self?

关于Rust内部可变性的API设计困惑(C++开发者视角)

我拥有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学习者的常见错误)。

回到我下方的示例代码,我面临两个选择:

  1. 将change_e()到change_i()改为使用&mut self(编译器不会报错,尽管并非必须),以与修改存储整数状态的事实保持一致性;
  2. 继续使用&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());
}

解答:内部可变性的合理使用与API设计原则

作为从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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.08 21:37:45