API设计中的内部变异性滥用?[英] Interior mutability abuse in API design?

本文是小编为大家收集整理的关于API设计中的内部变异性滥用?的处理/解决方法,可以参考本文帮助大家快速定位并解决问题,中文翻译不准确的可切换到English标签页查看源文。

问题描述

我的背景在c ++中,我对内部变形性表示不舒服. 以下代码是我对此主题的调查.

我同意,从借用检查员的角度来看,处理 许多关于内部状态的每个结构的引用 很快或之后被改变是不可能的;那清楚地在哪里 室内变形可以提供帮助.

此外,在第 15.5"Refcell和内部变形模式",示例 关于Messenger特征及其实施 MockMessenger struct让我认为这是一个共同的API 设计以系统地更喜欢&self甚至超过&mut self 如果它非常明显,某种可变的可变性将是强制性的 很快或以后. 如何实现Messenger不会改变其内部 发送消息时的状态? 异常只是打印邮件,这是一致的 使用&self,但常规案例可能包含在内 写给某种内部流,这可能意味着缓冲, 更新错误标志...... 所有这些都肯定需要&mut self,例如 impl Write for File .

依靠室内变形,解决这个问题听起来对我来说听起来 喜欢,在c ++,const_cast ing或滥用mutable成员 因为在申请中的其他地方我们并不一致 const NESS(C ++学习者的常见错误).

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

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

这个决定不仅是结构本身的本地,而且是遗嘱 对可以在中表达的东西有很大影响 应用使用此结构. 第二种解决方案肯定会有所帮助,因为只有 共享参考资料涉及,但它与什么符合 预计在锈症中.

我找不到这个问题的答案 rust api指南. 是否有其他生锈文件类似于 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());
}

推荐答案

依靠室内变形,解决这个问题听起来对我来说听起来 喜欢,在c ++,const_cast ing或滥用mutable成员 因为在申请中的其他地方我们并不一致 const NESS(C ++学习者的常见错误).

这是在C ++的背景下完全理解的思想.它不准确的原因是因为C ++和Rust具有不同的可变性概念.

在某种程度上,Rust的mut关键字实际上有两个含义.在模式中,它意味着"可变",并且在引用类型中,它意味着"独占". &self和&mut self之间的差异不是真正的是否可以突变self,但是否可以是 aliased .

在Messenger的例子中,嗯,首先让我们不太认真地服用;它意味着说明语言功能,不一定是系统设计.但是我们可以想象为什么可以使用&self:Messenger意味着通过共享的结构来实现,因此不同的代码可以将引用持有相同的对象并将其用于send警报而不互相协调.如果send要采用&mut self,则为此目的是没用的,因为一次只能存在一个&mut self参考.将消息发送到共享Messenger(不通过Mutex或其他东西添加外部内部变形性).

另一方面,每个 c ++引用和指针可以是别名的.¹如此,在c ++中的所有变形性是"内部"的可变性! RUDE在C ++中没有相当于mutable,因为锈蚀没有const成员(这里的乘量是"可变性是绑定的属性,而不是类型").生锈是否具有相当于const_cast,但仅针对原始指针,因为它是不合作的,可以将共享&参考转换为独占&mut参考.相反,C ++与Cell或RefCell相同,因为每个值都是隐式地在UnsafeCell后面.

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

它真的取决于Thing的预期语义.是Thing要共享的性质,如频道端点或文件? change_e在共享(aliased)参考上调用change_e是有意义的吗?如果是这样,则使用内部可变性暴露在&self上的方法. Thing主要是用于数据的容器?它有时会有意思是有意义的,有时候是独家的吗?然后Thing应该不应该使用内部变形性,并让图书馆的用户决定如何处理共享突变,应该是必要的.

另见


实际上,c ++ do 有一个功能,使指针与Rust中的引用类似.的种类. restrict是C ++中的非标准扩展,但它是C99的一部分. Rust的共享(&)引用类似于const *restrict指针,并且独占(&mut)引用就像非const *restrict指针.查看限制关键字均值在c ++中是什么?

您最后一次故意使用C ++中的restrict(或__restrict等)指针是什么时候?不要打扰思考它;答案是"永远不会". restrict使得能够比常规指针更具侵略性优化,但很难正确使用,因为您必须非常谨慎地对别名进行非常谨慎,并且编译器没有提供帮助.它基本上是一个巨大的脚跟,几乎没有人使用它.为了使您有价值地使用restrict在C ++中使用const中的方式,您需要能够向允许指向别名的函数进行注释,在此时别名,何时何时进行一些规则指针有效遵循,并具有编译器通过,该编译器通过检查每个功能中是否遵循规则.就像某种......检查员.

本文地址:https://www.itbaoku.cn/post/1937858.html

问题描述

My background in C++ makes me uncomfortable about interior mutability. The code below is my investigation around this topic.

I agree that, from the borrow checker point of view, dealing with many references on every single struct which internal state could be altered soon or later is impossible; that's clearly where interior mutability can help.

Moreover, in chapter 15.5 "RefCell and the Interior Mutability Pattern" of The Rust Programming Language, the example about the Messenger trait and its implementation on the MockMessenger struct makes me think that it is a common API design to systematically prefer &self over &mut self even if its quite obvious that some kind mutability will be mandatory soon or later. How could an implementation of Messenger not alter its internal state when sending a message? The exception is just printing the message, which is consistent with &self, but the general case would probably consist in writing to some kind of inner stream, which could imply buffering, updating error flags... All of this certainly requires &mut self, as for example impl Write for File.

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++).

So, back to my example code below, should I:

  • use &mut self (the compiler doesn't complain, even if it's not mandatory) from change_e() to change_i() in order to keep consistent with the fact that I alter the values of the stored integers?
  • keep using &self, because interior mutability allows it, even if I actually alter the values of the stored integers?

This decision is not only local to the struct itself but will have a large influence on what could be expressed in the application using this struct. The second solution will certainly help a lot, because only shared references are involved, but is it consistent with what is expected in Rust.

I cannot find an answer to this question in Rust API Guidelines. Is there any other Rust documentation similar to 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++).

This is a completely understandable thought in the context of C++. The reason it isn't accurate is because C++ and Rust have different concepts of mutability.

In a way, Rust's mut keyword actually has two meanings. In a pattern it means "mutable" and in a reference type it means "exclusive". The difference between &self and &mut self is not really whether self can be mutated or not, but whether it can be aliased.

In the Messenger example, well, first let's not take it too seriously; it's meant to illustrate the language features, not necessarily system design. But we can imagine why &self might be used: Messenger is meant to be implemented by structures that are shared, so different pieces of code can hold references to the same object and use it to send alerts without coordinating with each other. If send were to take &mut self, it would be useless for this purpose because there can only be one &mut self reference in existence at a time. It would be impossible to send messages to a shared Messenger (without adding an external layer of interior mutability via Mutex or something).

On the other hand, every C++ reference and pointer can be aliased.¹ So in Rust terms, all mutability in C++ is "interior" mutability! Rust has no equivalent to mutable in C++ because Rust has no const members (the catchphrase here is "mutability is a property of the binding, not the type"). Rust does have an equivalent to const_cast, but only for raw pointers, because it's unsound to turn a shared & reference into an exclusive &mut reference. Conversely, C++ has nothing like Cell or RefCell because every value is implicitly behind an UnsafeCell already.

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

It really depends on the intended semantics of Thing. Is it the nature of Thing to be shared, like a channel endpoint or a file? Does it make sense for change_e to be called on a shared (aliased) reference? If so, then use interior mutability to expose a method on &self. Is Thing primarily a container for data? Does it sometimes make sense for it to be shared and sometimes exclusive? Then Thing should probably not use interior mutability and let the user of the library decide how to deal with shared mutation, should it be necessary.

See also


¹ Actually, C++ does have a feature that makes pointers work similar to references in Rust. Kind of. restrict is a non-standard extension in C++ but it's part of C99. Rust's shared (&) references are like const *restrict pointers, and exclusive (&mut) references are like non-const *restrict pointers. See What does the restrict keyword mean in C++?

When was the last time you deliberately used a restrict (or __restrict, etc.) pointer in C++? Don't bother thinking about it; the answer is "never". restrict enables more aggressive optimizations than regular pointers, but it is very hard to use it correctly because you have to be extremely careful about aliasing, and the compiler offers no assistance. It's basically a massive footgun and hardly anyone uses it. In order to make it worthwhile to use restrict pervasively the way you use const in C++, you'd need to be able to annotate onto functions which pointers are allowed to alias other ones at which times, make some rules about when pointers are valid to follow, and have a compiler pass that checks whether the rules are being followed in each function. Like some kind of... checker.