3

我一直在思考为什么在大多数情况下 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);
    });
}

操场

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

1 回答 1

8

您的 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)]
   |
于 2020-04-11T17:55:02.607 回答