新手上路,随便写写,欢迎批评指正。
众所周知,Rust 中有两种引用,一种叫共享引用(shared references),一种叫可变引用(mutable references)。它们的区别大家可能早已了然于胸:
- 共享引用不能改,但可以被多人持有;
- 可变引用可以随便改,但只能被一人持有。
第二条暗示了:这个指针只有一个人拿着,那么(在安全 Rust 中)没有两个指针指向同一片区域。因此 noalias 上线。这给编译器优化打开了大门。(至今 C++ 没有 noalias 指针,而 C 有 restrict 关键字。)
第一条说法有待商榷。因为共享引用并不叫不可变引用(immutable references)。假设它是对的,那么 &T 也是 noalias 的。
现在我们发现了一个问题:所有指针都是 noalias 的。虽然我们可能希望 noalias 指针占比越重越好,但有时我们还真的想让多个人通过指针在同一块内存区域里面写来写去。怎么办?我们又真的不想放弃 &mut 的 noalias 特性,因为优化很重要,而且改回去和之前的语言又有什么区别?
此时,共享引用上线。共享引用的本质在于共享,而不在于不可变,不然为什么不叫不可变引用呢?那么现在问题来了,怎么让一个 noalias 的指针可以搞出来几个别名?很简单,我们再引入一层 indirection,比如说,这样
/// 这只是伪代码!
struct UnsafeCell<T> {
r: T // 只需想办法告诉编译器,这块地盘的指针可能有别名
// 为什么不用指针呢?因为能放栈上为啥不放栈上
}
impl<T> for UnsafeCell<T> {
unsafe fn get_ptr(&self) -> *mut T {
&self.r as *mut T
}
}
这样我们每次想用的时候,只需透过 UnsafeCell::get_ptr()
即可。这样,一个 UnsafeCell
实际上就代表一个地板(内存区域),这个地板你从来不会去挪动,但在上面放的东西可以拿来拿去。
那么问题又来了,这样乱搞是不是会破坏 Rust 的安全性保证呢?不会!因为 Rust 的最高优先级是避免数据竞争,我们现在把这样的「洞」都改成 !Sync,那当然不可能有数据竞争了。
现在我们来设计一下这个地板怎么用。我们大概有两种设计方法,顺便起个名字吧:
Cell
:把东西放在地板上,要么你拿走一份拷贝(get),要么你把你的数据给我,我拿这个地板上的东西交换(replace)。这种方法可以保证数据永远是完整的,不可能出现读到一半被改写的情况(!Sync)。RefCell
:你只能借用我在地板上放的东西,不过我允许你借走 &mut 让你去改。
第一种比较简单,Cell
就是个地板管理员,只放完整的东西。第二种有点危险,因为 RefCell
是被共享的,如果 &mut 随随便便都能借走,那 &mut 的 noalias 特性就功亏一篑了!怎么办?好办!运行时检查即可。「你乱搞内存后,我再也不用担心内存问题了,因为我 panic 了。」
总之,我们就这样从另一个角度重新发明了 Cell
和 RefCell
。回过头来看,UnsafeCell
有什么用呢?主要是取消 noalias 用的。这时可能会感觉到,诶,这个 *Cell
其实是个智能指针?
现在回到开头的第一条「&T
是不能改的」就发现有点问题,其实我可以改,只要被 *Cell
什么的包一下就行了。只要没有这些内部可变性,我们还是能期望不管你怎么读数据都不会变。然后你仔细一想,发现,诶,原来我们做了这么半天,其实就是用了个 !Sync
避免了数据竞争呀!说破天也不过是去掉了 noalias 而已,而 noalias 本身也是避免数据竞争的副产品……
(从某种角度来看,Rust 的做法和 Clojure 的 reference/agent 很像。)
后记
本文是 interior mutability 的另一种视角。一般来说,引入内部可变性是因为我们想让共享引用的 struct
内部可以被改写,但内部可变性的本质应该是非 noalias 的指针。只要有指针,你就能读读写写(至于语言本身加的限制暂且不论),凭什么一个指针被复制成另一个指针后就不能写了呢?
下面抛几个链接作为延伸阅读。
- https://www.reddit.com/r/rust/comments/4cvc3o/what_are_cell_and_refcell_used_for/d1m2j5r/
- https://ricardomartins.cc/2016/06/08/interior-mutability
评论区
写评论还没有评论