一种具有内部可变性的细胞,允许任意的突变行为



标准单元格结构提供内部可变性,但只允许一些突变方法,如set((、swap((和replace((。所有这些方法都会更改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的新手(。

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

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

[EDIT]我想要零成本的抽象,而不存在运行时失败的可能性,所以RefCell不是完美的解决方案。

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

  1. 它是安全的吗,也就是说,一些安全但恶意的闭包可以通过使用这个";通用的";单间牢房我认为它是安全的,因为内部引用参数的生存期禁止它在闭包调用时间之后进行公开。但我仍然心存疑虑(我是Rust的新手(

总之,没有

游乐场

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的最佳方法是将其分解为单独的Cells。例如,使用(Cell<i8>, Cell<i8>, Cell<i8>)而不是Cell<(i8, i8, i8)>

尽管如此,IMO、Cell很少是最好的解决方案。内部可变性在C和许多其他语言中是一种常见的设计,但在Rust中更为罕见,至少通过共享引用和Cell是这样,原因有很多(例如,它不是Sync,通常人们不希望没有&mut就有内部可变性(。问问自己为什么要使用Cell,如果真的不可能重新组织代码以使用普通的&mut引用。

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

  1. 有一种安全(但可能复杂/非直观(的方法可以做到这一点,或者
  2. 它实际上是未定义的行为(就像本例中一样(

编辑:Frxstrem的回答也提供了关于何时使用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允许您从不可变引用中获取可变引用,但需要在运行时执行检查以确保这是安全的

据我所知,没有任何安全的方法来解决这一问题,因此您需要在代码中做出选择,要么是无运行时成本但灵活性较低,要么是更灵活但运行时成本较低。

最新更新