具有内部可变性的细胞,允许任意突变动作
标准单元结构提供内部可变性,但只允许少数变异方法,如 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 初学者来说,这是一个非常常见的陷阱。
- 它是否安全,即一些安全但恶意的关闭是否可以通过使用这个“通用”单元来破坏 Rust 的可变性/借用/生命周期规则?我认为它是安全的,因为内部引用参数的生命周期禁止在闭包调用时间之后对其进行公开。但我仍然有疑问(我是 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),并且可能导致程序中任何地方的损坏/崩溃。
- 也许我正在重新发明轮子,并且存在一些解决问题的模板或技术?
使用Cell通常不是最好的技术,但如果不了解更多问题,就不可能知道最佳解决方案是什么。
如果您坚持Cell,有安全的方法可以做到这一点。不稳定(即测试版)Cell::update()方法实际上是用以下代码实现的(when 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.
但是,通常仅更改 a 的一部分的最佳方法Cell是将其分解为单独的Cells。例如,代替Cell<(i8, i8, i8)>,使用(Cell<i8>, Cell<i8>, Cell<i8>)。
尽管如此,IMOCell很少是最好的解决方案。内部可变性是 C 和许多其他语言中的常见设计,但它在 Rust 中更为罕见,至少通过共享引用和Cell,出于多种原因(例如,它不是Sync,并且通常人们不期望没有内部可变性&mut)。问问自己为什么要使用,Cell以及是否真的不可能重新组织代码以使用普通&mut引用。
IMO 的底线实际上是关于安全的:如果无论您做什么,编译器都会抱怨并且您似乎需要使用unsafe,那么我向您保证 99% 的时间是:
- 有一种安全(但可能复杂/不直观)的方法来做到这一点,或者
- 它实际上是未定义的行为(例如在这种情况下)。
编辑:Frxstrem 的回答也有关于何时使用Cell/ 的更好信息RefCell。
回答
您的代码不安全,因为您可以调用c.execinsidec.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!
})
})
事实上,这正是我们同时拥有Celland的原因RefCell:
Cell只允许您获取和设置一个值,而不允许您从不可变引用中获取可变引用(从而避免了上述问题),但它没有任何运行时成本。RefCell允许您从不可变引用中获取可变引用,但需要在运行时执行检查以确保这是安全的。
据我所知,实际上没有任何安全的方法可以解决这个问题,因此您需要在代码中做出选择:没有运行时成本但灵活性较低,以及灵活性更高但运行时成本较低。