rust - 具有内部可变性的细胞,允许任意突变 Action

标签 rust interior-mutability

标准Cell struct 提供内部可变性,但只允许少数变异方法,例如 set()、swap() 和 replace()。所有这些方法都会改变 Cell 的整个内容。
但是,有时需要更具体的操作,例如,仅更改 Cell 中包含的部分数据。
所以我尝试实现某种通用 Cell,允许任意数据操作。
操作由用户定义的闭包表示,该闭包接受单个参数 - &mut 对 Cell 内部数据的引用,因此用户自己可以决定如何处理 Cell 内部。下面的代码演示了这个想法:

use std::cell::UnsafeCell;

struct MtCell<Data>{
    dcell: UnsafeCell<Data>,
}

impl<Data> MtCell<Data>{
    fn new(d: Data) -> MtCell<Data> {
        return MtCell{dcell: UnsafeCell::new(d)};
    }

    fn exec<F, RetType>(&self, func: F) -> RetType where
        RetType: Copy,
        F: Fn(&mut Data) -> RetType 
    {
        let p = self.dcell.get();
        let pd: &mut Data;
        unsafe{ pd = &mut *p; }
        return func(pd);
    }
}

// test:

type MyCell = MtCell<usize>;

fn main(){
    let c: MyCell = MyCell::new(5);
    println!("initial state: {}", c.exec(|pd| {return *pd;}));
    println!("state changed to {}", c.exec(|pd| {
        *pd += 10; // modify the interior "in place"
       return *pd;
    }));
}
但是,我对代码有一些担忧。
  • 它是否安全,即一些安全但恶意的关闭是否可以通过使用这个“通用”单元来破坏 Rust 的可变性/借用/生命周期规则?
    我认为它是安全的,因为内部引用参数的生命周期禁止在闭包调用时间之后对其进行公开。但我仍然有疑问(我是 Rust 的新手)。
  • 也许我正在重新发明轮子,并且存在一些解决问题的模板或技术?

  • 注意:我在这里发布了这个问题(不是关于代码审查),因为它似乎与语言而不是代码本身更相关(这只是一个概念)。
    [编辑] 我想要零成本抽象而没有运行时故障的可能性,所以 RefCell 不是完美的解决方案。

    最佳答案

    对于 Rust 初学者来说,这是一个非常常见的陷阱。

    1. Is it safe, i.e can some safe but malicious closure break Rust mutability/borrowing/lifetime rules by using this "universal" cell? I consider it safe since lifetime of the interior reference parameter prohibits its exposition beyond the closure call time. But I still have doubts (I'm new to Rust).

    一句话,没有。
    Playground
    fn main() {
        let mt_cell = MtCell::new(123i8);
    
        mt_cell.exec(|ref1: &mut i8| {
            mt_cell.exec(|ref2: &mut i8| {
                println!("Double mutable ref!: {:?} {:?}", ref1, ref2);
            })
        })
    }
    
    引用不能在闭包外使用是绝对正确的,但在闭包内,所有赌注都取消了!事实上,闭包内的单元格上的几乎任何操作(读或写)都是未定义行为(UB),并且可能导致程序中任何地方的损坏/崩溃。
    1. Maybe I'm re-inventing the wheel and there exist some templates or techniques solving the problem?

    使用 Cell通常不是最好的技术,但如果不了解更多问题,就不可能知道最佳解决方案是什么。
    如果你坚持Cell ,有安全的方法可以做到这一点。不稳定的(即测试版)Cell::update()方法实际上是用以下代码实现的(当 T: Copy 时):
    pub fn update<F>(&self, f: F) -> T
    where
        F: FnOnce(T) -> T,
    {
        let old = self.get();
        let new = f(old);
        self.set(new);
        new
    }
    
    或者你可以使用 Cell::get_mut() ,但我想这违背了 Cell 的全部目的.
    但是,通常最好的方法是只更改 Cell 的一部分。是把它分解成单独的 Cell s。例如,代替 Cell<(i8, i8, i8)> , 使用 (Cell<i8>, Cell<i8>, Cell<i8>) .
    尽管如此,IMO,Cell很少是最好的解决方案。内部可变性是 C 和许多其他语言中的常见设计,但在 Rust 中更为罕见,至少通过共享引用和 Cell ,出于多种原因(例如,它不是 Sync ,并且通常人们不期望没有 &mut 的内部可变性)。问问自己为什么要使用 Cell如果真的无法重新组织您的代码以使用正常 &mut引用。
    IMO 的底线实际上是关于安全的:如果不管你做什么,编译器都会提示,看来你需要使用 unsafe ,那么我向您保证 99% 的时间:
  • 有一种安全(但可能复杂/不直观)的方法来做到这一点,或者
  • 它实际上是未定义的行为(例如在这种情况下)。

  • 编辑 :Frxstrem's answer还有关于何时使用的更好信息 Cell/RefCell .

    关于rust - 具有内部可变性的细胞,允许任意突变 Action ,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/65374356/

    相关文章:

    recursion - 有没有办法优化这段代码,使其不会溢出堆栈?

    android - Android NDK(libgcc_real.a)上 '__aeabi_ul2f'的多个定义

    data-structures - 一个模块中的 Rc<T>s 和另一个模块中的 Rc<RefCell<T>>s 引用相同的数据

    reference - RefCell<X> 和 RefCell<&X> 上的 borrow_mut 之间的区别

    rust - 可变借用后的不可变引用

    rust - 如何将负 i32 数添加到 usize 变量?

    closures - 在 Rust 中使用标准输入数据实例化结构

    rust - 如何在不破坏封装的情况下返回对 RefCell 中某些内容的引用?

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