rust - API 设计中的内部可变性滥用?

标签 rust api-design interior-mutability

我在 C++ 方面的背景让我对内部可变性感到不舒服。
下面的代码是我围绕这个主题的调查。
我同意,从借用检查器的角度来看,处理
每个内部状态可以的单个结构上的许多引用
迟早要改变是不可能的;这显然是在哪里
内部可变性可以提供帮助。
此外,在章节15.5 "RefCell and the Interior Mutability Pattern" Rust 编程语言的例子
关于Messenger trait 及其在MockMessenger struct 让我觉得它是一个通用的 API
设计系统偏好&self超过 &mut self甚至
如果很明显某种可变性将是强制性的
迟早。Messenger 的实现怎么可能?不改变其内部
发送消息时的状态?
异常(exception)只是打印消息,这是一致的
&self , 但一般情况可能包括
写入某种内部流,这可能意味着缓冲,
更新错误标志...
所有这些当然需要&mut self ,例如
impl Write for File .
在我看来,依靠内部可变性来解决这个问题
例如,在 C++ 中,const_cast滥用或滥用mutable成员只是
因为在应用程序的其他地方我们并不一致const ness(C++ 学习者的常见错误)。
那么,回到下面的示例代码,我应该:

  • 使用 &mut self (编译器不会提示,即使它是
    非强制性)来自 change_e()change_i()为了
    与我改变值的事实保持一致
    存储整数?
  • 继续使用&self ,因为内部可变性允许它,甚至
    如果我真的改变了存储整数的值?

  • 这个决定不仅是结构本身的本地决定,而且会
    对可以表达的内容有很大的影响
    使用此结构的应用程序。
    第二种解决方案肯定会有很大帮助,因为只有
    涉及共享引用,但它是否与
    预计在 Rust 中。
    我找不到这个问题的答案
    Rust API Guidelines .
    是否有任何其他类似的 Rust 文档
    C++CoreGuidelines ?
    /*
        $ 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 // & is enough (read-only)
        {
            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 is mandatory
        {
            let target = &mut self.a;
            *target += 10;
        }
    
        fn change_b(&mut self) // &mut is mandatory
        {
            let target = self.b.as_mut();
            *target += 20;
        }
    
        fn change_c(&mut self) // &mut is mandatory
        {
            let target = std::rc::Rc::get_mut(&mut self.c).unwrap();
            *target += 30;
        }
    
        fn change_d(&mut self) // &mut is mandatory
        {
            let target = std::sync::Arc::get_mut(&mut self.d).unwrap();
            *target += 40;
        }
    
        fn change_e(&self) // !!! no &mut here !!!
        {
            // With C++, a std::mutex protecting a separate integer (e)
            // would have been used as two data members of the structure.
            // As our intent is to alter the integer (e), and because
            // std::mutex::lock() is _NOT_ const (but it's an internal
            // that could have been hidden behind the mutable keyword),
            // this member function would _NOT_ be const in C++.
            // But here, &self (equivalent of a const member function)
            // is accepted although we actually change the internal
            // state of the structure (the protected integer).
            let mut target = self.e.lock().unwrap();
            *target += 50;
        }
    
        fn change_f(&self) // !!! no &mut here !!!
        {
            // actually alters the integer (as with e)
            let mut target = self.f.write().unwrap();
            *target += 60;
        }
    
        fn change_g(&self) // !!! no &mut here !!!
        {
            // actually alters the integer (as with e, f)
            let target = self.g.get();
            unsafe { *target += 70 };
        }
    
        fn change_h(&self) // !!! no &mut here !!!
        {
            // actually alters the integer (as with e, f, g)
            self.h.set(self.h.get() + 80);
        }
    
        fn change_i(&self) // !!! no &mut here !!!
        {
            // actually alters the integer (as with 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());
    }
    

    最佳答案

    Relying on interior mutability to solve this problem sounds to me like, in C++, const_casting or abusing of mutable members just because elsewhere in the application we were not consistent about constness (common mistake for learners of C++).


    在 C++ 的上下文中,这是一个完全可以理解的想法。它不准确的原因是因为 C++ 和 Rust 有不同的可变性概念。
    在某种程度上,Rust 的 mut 关键字实际上有两个含义。在模式中,它表示“可变”,而在引用类型中,它表示“独占”。 &self&mut self 的区别其实不在于 self 是否可以变异,而在于是否可以别名。
    Messenger 示例中,首先我们不要太认真;它旨在说明语言功能,不一定是系统设计。但是我们可以想象为什么可能会使用 &self:Messenger 旨在由共享的结构实现,因此不同的代码片段可以保存对同一对象的引用并将其用于 send 警报,而无需相互协调。如果 send 采用 &mut self ,那么对于此目的将毫无用处,因为一次只能存在一个 &mut self 引用。将消息发送到共享的 Messenger 是不可能的(不通过 Mutex 或其他东西添加内部可变性的外部层)。
    另一方面,每个 C++ 引用和指针都可以有别名。¹因此,在 Rust 术语中,C++ 中的所有可变性都是“内部”可变性! Rust 没有与 C++ 中的 mutable 等价物,因为 Rust 没有 const 成员(这里的流行语是“可变性是绑定(bind)的属性,而不是类型”)。 Rust 确实有 const_cast 等价物,但仅适用于原始指针,因为将共享 & 引用转换为独占 &mut 引用是不合理的。相反,C++ 没有 CellRefCell 之类的东西,因为每个值都隐含在 UnsafeCell 后面。

    So, back to my example code below, should I[...]


    这实际上取决于 Thing 的预期语义。 Thing 的本质是要共享的,比如 channel 端点还是文件?在共享(别名)引用上调用 change_e 是否有意义?如果是这样,则使用内部可变性在 &self 上公开方法。 Thing 主要是数据容器吗?有时共享有时排他有意义吗?然后 Thing 可能不应该使用内部可变性,并让库的用户决定如何处理共享突变,如果有必要的话。
    也可以看看
  • Accurate mental model for Rust's reference types
  • What's the difference between placing "mut" before a variable name and after the ":"?
  • Why is the mutability of a variable not reflected in its type signature in Rust?
  • Need holistic explanation about Rust's cell and reference counted types

  • ¹ 实际上,C++ 确实有一个特性,它使指针的工作方式类似于 Rust 中的引用。有点儿。 restrict 是 C++ 中的非标准扩展,但它是 C99 的一部分。 Rust 的共享 (&) 引用类似于 const *restrict 指针,而独占 (&mut) 引用类似于非 const *restrict 指针。见 What does the restrict keyword mean in C++?
    您最后一次在 C++ 中故意使用 restrict(或 __restrict 等)指针是什么时候?不要费心去想它;答案是“从不”。 restrict 可以比常规指针进行更积极的优化,但是很难正确使用它,因为您必须非常小心别名,并且编译器不提供任何帮助。它基本上是一把巨大的脚枪,几乎没有人使用它。为了使在 C++ 中使用 restrict 的方式普遍使用 const 是值得的,您需要能够在函数上注释哪些指针可以在什么时候给其他指针起别名,制定一些关于指针何时有效的规则遵循,并有一个编译器通过检查每个函数是否遵循规则。就像某种...检查器。

    关于rust - API 设计中的内部可变性滥用?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/63487359/

    相关文章:

    rust - 为什么链接生存期仅与可变引用有关?

    Java API设计: narrow return type in methods

    c++ - 在命名空间声明之后声明 using 语句

    reference - 我如何返回一个引用了 RefCell 中某些内容的迭代器?

    rust - 是否有替代方法或方法让 Rc<RefCell<X>> 限制 X 的可变性?

    rust - 如何在多个 "encoders"之间共享可变写入器?

    reference - 可变引用生命周期与不可变引用生命周期

    rust - 在 Rust 中实现字符串缓冲区的最佳方式是什么?

    rust - 从子 iter().map 中的父 iter().map 访问变量会抛出 'error[E0597]: ` x` does not live long enough' on compile'

    java - 包装器的 parseXXX() 用于签名二进制误解