为什么我的 RefCell 零成本替代方案不是实现内部可变性的标准方法?
Why is my zero-cost alternative to RefCell not the standard way of achieving interior mutability?
我一直在思考为什么在大多数情况下 Rust 的内部可变性需要运行时检查(例如 RefCell
)。看起来我找到了一个没有运行时成本的安全替代方案。我将类型命名为 SafeCell
(主要是因为它是 UnsafeCell
的安全包装器),它允许您将任何函数应用于包装的值,而不会有引用转义的风险:
struct SafeCell<T> {
inner: UnsafeCell<T>,
}
impl<T> SafeCell<T> {
pub fn new(value: T) -> Self {
Self {
inner: UnsafeCell::new(value),
}
}
pub fn apply<R, F>(&self, fun: F) -> R
where
F: FnOnce(&mut T) -> R,
{
// Reference below has a lifetime of the current scope, so if
// user tries to save it somewhere, borrow checker will catch this.
let reference: &mut T = unsafe { &mut *self.inner.get() };
fun(reference)
}
}
此类型可用于内部可变性,如下所示:
pub struct MySet {
set: HashSet<i32>,
unique_lookups: SafeCell<HashSet<i32>>,
}
impl MySet {
pub fn contains(&self, value: i32) -> bool {
self.unique_lookups.apply(|lookups| lookups.insert(value));
self.set.contains(value)
}
pub fn unique_lookups_count(&self) -> usize {
self.unique_lookups.apply(|lookups| lookups.len())
}
}
或结合Rc
:
fn foo(rc: Rc<SafeCell<String>>) {
rc.apply(|string| {
if string.starts_with("hello") {
string.push_str(", world!")
}
println!("{}", string);
});
}
- 这种类型有 safety/soundness 问题吗?
- 如果不是,为什么这样的类型不是实现内部可变性的标准方法?看起来它与
RefCell
一样可用,同时提供静态生命周期检查而不是运行时检查。
您的 API 中没有任何内容可以阻止用户在提供给 apply
的闭包中再次调用 apply
。这允许对同一数据有多个同时可变引用,这是未定义的行为。
let x = SafeCell::new(0);
x.apply(|y| {
x.apply(|z| {
// `y` and `z` are now both mutable references to the same data
// UB!
*y = 1;
*z = 2;
})
});
x.apply(|y| println!("x: {}", y));
Miri 在看到正在生成的第二个可变引用时正确地调用了它。
error: Undefined Behavior: not granting access to tag <untagged> because incompatible item is protected: [Unique for <1651> (call 1230)]
--> src/main.rs:20:42
|
20 | let reference: &mut T = unsafe { &mut *self.inner.get() };
| ^^^^^^^^^^^^^^^^^^^^^^ not granting access to tag <untagged> because incompatible item is protected: [Unique for <1651> (call 1230)]
|
我一直在思考为什么在大多数情况下 Rust 的内部可变性需要运行时检查(例如 RefCell
)。看起来我找到了一个没有运行时成本的安全替代方案。我将类型命名为 SafeCell
(主要是因为它是 UnsafeCell
的安全包装器),它允许您将任何函数应用于包装的值,而不会有引用转义的风险:
struct SafeCell<T> {
inner: UnsafeCell<T>,
}
impl<T> SafeCell<T> {
pub fn new(value: T) -> Self {
Self {
inner: UnsafeCell::new(value),
}
}
pub fn apply<R, F>(&self, fun: F) -> R
where
F: FnOnce(&mut T) -> R,
{
// Reference below has a lifetime of the current scope, so if
// user tries to save it somewhere, borrow checker will catch this.
let reference: &mut T = unsafe { &mut *self.inner.get() };
fun(reference)
}
}
此类型可用于内部可变性,如下所示:
pub struct MySet {
set: HashSet<i32>,
unique_lookups: SafeCell<HashSet<i32>>,
}
impl MySet {
pub fn contains(&self, value: i32) -> bool {
self.unique_lookups.apply(|lookups| lookups.insert(value));
self.set.contains(value)
}
pub fn unique_lookups_count(&self) -> usize {
self.unique_lookups.apply(|lookups| lookups.len())
}
}
或结合Rc
:
fn foo(rc: Rc<SafeCell<String>>) {
rc.apply(|string| {
if string.starts_with("hello") {
string.push_str(", world!")
}
println!("{}", string);
});
}
- 这种类型有 safety/soundness 问题吗?
- 如果不是,为什么这样的类型不是实现内部可变性的标准方法?看起来它与
RefCell
一样可用,同时提供静态生命周期检查而不是运行时检查。
您的 API 中没有任何内容可以阻止用户在提供给 apply
的闭包中再次调用 apply
。这允许对同一数据有多个同时可变引用,这是未定义的行为。
let x = SafeCell::new(0);
x.apply(|y| {
x.apply(|z| {
// `y` and `z` are now both mutable references to the same data
// UB!
*y = 1;
*z = 2;
})
});
x.apply(|y| println!("x: {}", y));
Miri 在看到正在生成的第二个可变引用时正确地调用了它。
error: Undefined Behavior: not granting access to tag <untagged> because incompatible item is protected: [Unique for <1651> (call 1230)]
--> src/main.rs:20:42
|
20 | let reference: &mut T = unsafe { &mut *self.inner.get() };
| ^^^^^^^^^^^^^^^^^^^^^^ not granting access to tag <untagged> because incompatible item is protected: [Unique for <1651> (call 1230)]
|