【问题标题】:Interior mutability abuse in API design?API 设计中的内部可变性滥用?
【发布时间】:2020-12-08 17:52:19
【问题描述】:

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

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

此外,在 The Rust Programming Language15.5 "RefCell and the Interior Mutability Pattern" 章节中,示例 关于 Messenger 特征及其在 MockMessenger struct 让我觉得它是一个通用的 API 设计系统地更喜欢&self而不是&mut self 如果很明显某种可变性将是强制性的 迟早。 Messenger 的实现如何不改变其内部 发送消息时的状态? 例外只是打印消息,这是一致的 &self,但一般情况可能包括 写入某种内部流,这可能意味着缓冲, 更新错误标志... 所有这些当然需要&mut self,例如 impl Write for File.

在我看来,依靠内部可变性来解决这个问题 就像,在 C++ 中,const_casting 或滥用 mutable 成员只是 因为在应用程序的其他地方我们并不一致 constness(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());
}

【问题讨论】:

  • 内部可变性只有在你不能这样做的情况下才应该使用,例如互斥锁使用它,因为没有它就无法工作。在应用程序代码中很少使用它,出于显而易见的原因,人们应该尽量避免它。
  • @Stargateur 那么,我是否应该认为本书中的Messenger trait 示例具有误导性?设计这样的特征意味着强制实现依赖于内部可变性。
  • 没有信使特征是“我们有一个不应该需要改变状态的特征”但用户想要,所以用户的解决方案是具有内部可变性,比如在示例中以跟踪过去的消息。
  • 请注意,虽然Write trait 确实使用了&amp;mut self,但File 本身实际上没有。您可以使用implementation for &amp;'_ File 写入和读取&amp;File。 (这不涉及内部可变性;这只是底层 OS API 的工作方式。)

标签: rust api-design interior-mutability


【解决方案1】:

在我看来,依靠内部可变性来解决这个问题 就像,在 C++ 中,const_casting 或滥用 mutable 成员只是 因为在应用程序的其他地方我们并不一致 constness(C++ 学习者的常见错误)。

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

在某种程度上,Rust 的 mut 关键字实际上有两个含义。在模式中,它表示“可变”,而在引用类型中,它表示“独占”。 &amp;self&amp;mut self 的区别并不在于self 是否可以变异,而在于它是否可以别名

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

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

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

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

另见


¹ 实际上,C++ 确实 具有使指针的工作方式类似于 Rust 中的引用的特性。的种类。 restrict 是 C++ 中的非标准扩展,但它是 C99 的一部分。 Rust 的共享(&amp;)引用类似于const *restrict 指针,而独占(&amp;mut)引用类似于非const*restrict 指针。见What does the restrict keyword mean in C++?

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

【讨论】:

  • 感谢您的详细解答。即使我知道非/mut 引用的共享/独占等价,我也没有意识到,在 API 设计方面,我们应该优先关注共享/独占方面,并且非/可变性只是这种选择的结果。您的澄清将极大地有益于我的设计选择,因为我的想法正好相反(非/可变性→共享/独占访问)!
  • 作为奖励(我什至没有要求它;^),您确认了 引用的 restrict 方面我期待但不确定它是 在 Rust 中隐含(我实际上在我的 C++ 模拟代码中广泛使用了__restrict__)。再次感谢您所有宝贵的解释。
  • sigh 我知道如果我在里面写上关于从不使用 restrict 的内容,我会发现我是在和 one 人交谈谁经常使用它! :-P
  • 这个问题我再好不过了,你的回答太棒了!
  • 可变性与混叠角度的典型解释是这篇文章:docs.rs/dtolnay/0.0.9/dtolnay/macro._02__reference_types.html
猜你喜欢
  • 1970-01-01
  • 2011-04-10
  • 2020-12-30
  • 2015-11-24
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-12-22
  • 1970-01-01
相关资源
最近更新 更多