为啥我的 RefCell 的零成本替代方案不是实现内部可变性的标准方法?

Posted

技术标签:

【中文标题】为啥我的 RefCell 的零成本替代方案不是实现内部可变性的标准方法?【英文标题】:Why is my zero-cost alternative to RefCell not the standard way of achieving interior mutability?为什么我的 RefCell 的零成本替代方案不是实现内部可变性的标准方法? 【发布时间】:2020-07-24 08:07:47 【问题描述】:

我一直在思考为什么 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);
    );

Playground

    这种类型是否存在任何安全/健全问题? 如果不是,为什么这样的类型不是实现内部可变性的标准方法?它看起来像 RefCell 一样可用,同时提供静态生命周期检查而不是运行时检查。

【问题讨论】:

【参考方案1】:

在提供给 apply 的闭包中,您的 API 中没有任何内容阻止用户再次调用 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));

(playground)

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)]
   |

【讨论】:

有关使用此想法的数据竞赛示例,请查看this。

以上是关于为啥我的 RefCell 的零成本替代方案不是实现内部可变性的标准方法?的主要内容,如果未能解决你的问题,请参考以下文章

当我可以使用Cell或RefCell时,我应该选择哪个?

为啥 console.log 从日志中排除我的零?

为啥 sortByKey 的火花这么慢?他们有啥替代方案吗?

为啥预处理器宏是邪恶的,有啥替代方案?

在实现 INotifyPropertyChanged 时,[CallerMemberName] 与替代方案相比是不是慢?

在实现 INotifyPropertyChanged 时,[CallerMemberName] 与替代方案相比是不是慢?