微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

具有内部可变性的细胞,允许任意突变动作

如何解决具有内部可变性的细胞,允许任意突变动作

标准 Cell 结构体提供内部可变性,但只允许少数变异方法,如 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;
    }));
}

但是,我对代码有些担忧。

  1. 它是否安全,即一些安全但恶意的关闭是否可以通过使用这个“通用”单元来破坏 Rust 的可变性/借用/生命周期规则? 我认为它是安全的,因为内部引用参数的生命周期禁止在闭包调用时间之后对其进行公开。但我仍然有疑问(我是 Rust 的新手)。

  2. 也许我正在重新发明轮子,并且存在一些解决问题的模板或技术?

注意:我在这里发布了这个问题(不是关于代码审查),因为它似乎与语言而非代码本身更相关(这只是一个概念)。

[编辑] 我想要零成本抽象而没有运行时故障的可能性,所以 RefCell 不是完美的解决方案。

解决方法

这是 Rust 初学者非常的一个陷阱。

  1. 它是否安全,即一些安全但恶意的关闭是否可以通过使用这个“通用”单元来破坏 Rust 的可变性/借用/生命周期规则?我认为它是安全的,因为内部引用参数的生命周期禁止在闭包调用时间之后对其进行公开。但我仍然有疑问(我是 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. 也许我正在重新发明轮子,并且存在一些解决问题的模板或技术?

使用 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。例如,使用 Cell<(i8,i8,i8)> 代替 (Cell<i8>,Cell<i8>,Cell<i8>)

不过,IMO,Cell 很少是最佳解决方案。内部可变性在 C 和许多其他语言中是一种常见的设计,但在 Rust 中比较少见,至少通过共享引用和 Cell,出于多种原因(例如它不是 Sync,和一般来说,如果没有 &mut,人们不会期望内部可变性。问问自己为什么要使用 Cell 以及是否真的无法重新组织您的代码以使用正常的 &mut 引用。

IMO 的底线实际上是关于安全的:如果无论您做什么,编译器都会抱怨并且您似乎需要使用 unsafe,那么我向您保证 99% 的时间:>

  1. 有一种安全(但可能复杂/不直观)的方法,或者
  2. 这实际上是未定义的行为(例如在这种情况下)。

EDITFrxstrem's answer 还提供了关于何时使用 Cell/RefCell 的更好信息。

,

您的代码不安全,因为您可以在 c.exec 中调用 c.exec 以获取对单元格内容的两个可变引用,如此仅包含安全代码的代码段所示:

let c: MyCell = MyCell::new(5);
c.exec(|n| {
    // need `RefCell` to access mutable reference from within `Fn` closure
    let n = RefCell::new(n);

    c.exec(|m| {
        let n = &mut *n.borrow_mut();
        
        // now `n` and `m` are mutable references to the same data,despite using
        // no unsafe code. this is BAD!
    })
})

事实上,这正是我们同时拥有 CellRefCell 的原因:

  • Cell 只允许你获取和设置一个值,不允许你从不可变的引用中获取可变引用(从而避免了上述问题),但它没有任何运行时成本。立>
  • RefCell 允许您从不可变引用中获取可变引用,但需要在运行时执行检查以确保这是安全的。

据我所知,实际上没有任何安全的方法可以解决这个问题,因此您需要在代码中做出选择:无运行时成本但灵活性较低,以及灵活性更高但运行时成本较低。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。