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

pro*_*-fh 6 api-design rust interior-mutability

我的 C++ 背景让我对内部可变性感到不舒服。下面的代码是我围绕这个主题的调查。

我同意,从借用检查器的角度来看,处理每个内部状态可能迟早改变的单个结构上的许多引用是不可能的;这显然是内部可变性可以提供帮助的地方。

此外,在第15.5“RefCell 和内部可变性模式”锈病编程语言,有关的例子Messenger特质及其对实施 MockMessenger结构让我觉得,这是一种常见的API设计,系统地喜欢&self&mut self,即使它很明显,迟早会强制要求某种可变性。Messenger发送消息时如何实现不改变其内部状态?例外只是打印消息,这与 一致&self,但一般情况可能包括写入某种内部流,这可能意味着缓冲,更新错误标志......所有这些当然都需要&mut self,例如 impl Write for File.

依靠内部的可变性来解决这个问题听起来好像是,在C ++中,const_cast荷兰国际集团或滥用mutable成员只是因为在申请的其他地方,我们是不是一致 const岬(对于C ++的学习者常犯的错误)。

所以,回到我下面的示例代码,我应该:

  • 使用&mut self(编译器不会抱怨,即使它不是强制性的)从change_e()change_i()为了与我改变存储的整数值的事实保持一致?
  • 继续使用&self,因为内部可变性允许它,即使我实际上改变了存储的整数的值?

这个决定不仅对结构本身来说是局部的,而且会对使用这个结构的应用程序中可以表达的内容产生很大的影响。第二种解决方案肯定会有很大帮助,因为只涉及共享引用,但它是否与 Rust 中的预期一致。

我在Rust API Guidelines 中找不到这个问题的答案 。是否有任何其他类似于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 // & 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());
}
Run Code Online (Sandbox Code Playgroud)

tre*_*tcl 11

依靠内部的可变性来解决这个问题听起来好像是,在C ++中,const_cast荷兰国际集团或滥用mutable成员只是因为在申请的其他地方,我们是不是一致 const岬(对于C ++的学习者常犯的错误)。

在 C++ 的上下文中,这是一个完全可以理解的想法。它不准确的原因是 C++ 和 Rust 有不同的可变性概念。

在某种程度上,Rust 的mut关键字实际上有两个含义。在模式中它意味着“可变”,在引用类型中它意味着“独占”。之间的区别&self,并&mut self没有真正是否self可以突变或不是,但它是否可以别名

在这个Messenger例子中,首先我们不要太认真;它旨在说明语言功能,而不一定是系统设计。但是我们可以想象为什么&self可能会使用:Messenger意味着由共享的结构实现,因此不同的代码段可以保存对同一对象的引用并使用它来send发出警报,而无需相互协调。如果send要采用&mut self,则对于此目的将毫无用处,因为一次只能存在一个&mut self引用。将消息发送到共享是不可能的Messenger(不通过Mutex或其他方式添加外部内部可变性层)。

另一方面,每个C++ 引用和指针都可以是别名。¹ 所以在 Rust 术语中,C++ 中的所有可变性都是“内部”可变性!Rustmutable在 C++ 中没有等价物,因为 Rust 没有const成员(这里的口号是“可变性是绑定的属性,而不是类型”)。Rust确实有一个等价于const_cast,但仅适用于原始指针,因为将共享&引用转换为独占&mut引用是不合理的。相反,C++ 没有类似CellRefCell因为每个值都隐含在一个UnsafeCell已经之后。

所以,回到我下面的示例代码,我应该[...]

这真的取决于预期的语义Thing。它Thing是共享的性质,如通道端点或文件?change_e在共享(别名)引用上调用是否有意义?如果是这样,则使用内部可变性在 上公开方法&self。是Thing主要用于数据的容器?有时共享,有时独占是否有意义?那么Thing可能不应该使用内部可变性并让库的用户决定如何处理共享突变,如果有必要的话。

也可以看看


¹ 实际上,C++确实有一个特性,它使指针的工作方式类似于 Rust 中的引用。的种类。restrict是 C++ 中的非标准扩展,但它是 C99 的一部分。Rust 的共享 ( &) 引用就像const *restrict指针,而独占 ( &mut) 引用就像非const *restrict指针。请参阅C++ 中的限制关键字是什么意思?

您最后一次在 C++ 中故意使用restrict(或__restrict等)指针是什么时候?不要费心去想它;答案是“从不”。restrict启用比常规指针更积极的优化,但很难正确使用它,因为您必须非常小心别名,并且编译器不提供任何帮助。它基本上是一个巨大的步枪,几乎没有人使用它。为了让restrictconst在 C++ 中使用的方式值得普遍使用,你需要能够在函数上注释哪些指针在什么时候可以别名其他指针,制定一些关于指针何时有效的规则,并有一个编译器通过检查每个函数中是否遵循规则。就像某种......检查器。


归档时间:

查看次数:

677 次

最近记录:

5 年,8 月 前