摘要:Rust 内部可变性概念:
UnsafeCell/Cell/RefCell 概念,用法及内部机制
*引言
fn main() {
// let x = 1;
let mut x = 1;
x += 1;
println!("The value of x is {}.", x);
}
// 如果我想更新函数中的变量,但又不想传递所有权时---使用一个可变的引用
fn add_and_print(x: &mut i32) {
*x += 1;
println!("The value of x in add_and_print is {}.", x); // 2
// 引用继承了直接从当前作用域向下传递的可变性---与下面要说的内部可变性相反
}
fn main() {
let mut x = 1;
add_and_print(&mut x);
x += 1;
println!("The value of x in main is {}.", x); // 3
}
// 包含对 i32 的可变引用的结构,因此所有权不会改变
struct XStruct<'a> {
x: &'a mut i32,
}
fn main() {
let mut x = 1;
let x_struct = XStruct { x: &mut x };
// x += 1; // Modify x... 不能这么做
println!("The value of x_struct is {:?}.", x_struct); // 1
println!("The value of x is {:?}.", x); // 1
}
// 为什么不能这么做? 回顾一下 Rust 的借用规则:
// 可以有一个可变引用
// 可以有多个不可变引用
// 引用总是有效的
// 在这个例子中 我们有两个对 的引用,我们希望这些引用是可变的。
//(是的,我知道,不是借来的,拥有它的价值,但我仍然认为它能够改变它自己的数据)
// 与借用规则相驳,原因是为了防止数据竞争,这可能导致一些非确定性错误---不展开讲述
// 基于这些规则,我想要多个可变的引用---内部可变性
// 内部可变性是Rust的设计模式之一
// 它允许我在只持有不可变引用的前提下对数据进行修改---类似的行为会被借用规则所禁止
// 但是为了改变数据,内部可变性在它的数据结构里面使用了unsafe代码来绕过Rust正常的可变性和借用规则---先不细说,继续往下看
// 实现内部可变性的方式---UnsafeCell、Cell、ReCell
一、内部可变性简介
内部可变性,有时候也叫共享可变性,就是说它持有共享引用又可以改变数据。
让程序拥有多个共享引用的时候还可以修改引用的数据。比如对外看起来是不变的接口,内部仍然有改变。
二、UnsafeCell 基本用法及内部机制
有一个get方法,通过不可变引用(&T),获取到了可变裸指针 (*mut T),它是通过强转来实现的
pub const fn get(&self) -> *mut T {
self as *const UnsafeCell<T> as *const T as *mut T
}
UnsafeCell使用了#[repr(transparent)]属性宏,显示指定了UnsafeCell内存结构必须与T相同,所以完全可以转换UnsafeCell到T。
需要注意的是,*const UnsafeCell as *mut T,未定义行为,需要通过UnsafeCell的get方法。
从名字看是不安全的,实际上它是安全的,因为它的方法全都没有用unsafe修饰,只是它的核心方法get返回的是可变裸指针。
use std::cell::UnsafeCell;
fn main() {
let cell = UnsafeCell::new(12);
println!("{:?}", cell.get());
}
// 强转到裸指针是安全的,不安全性体现在:如何正确的对裸指针进行解引用
// 因为裸指针没有借用和生命周期的检查,需要自己去注意它的生命周期与读写冲突
// UnsafeCell使用了#[lang = "unsafe_cell"]属性宏,这表示编译器需要对UnsafeCell进行特殊处理
// 编译器使得UnsafeCell对生命周期参数没有协/逆变特性,也就是说它是不变的
fn test_cell<'long: 'short, 'short>(mut cell: UnsafeCell<&'short i32>, cell2: UnsafeCell<&'long i32>) {
cell = cell2; // 错误!UnsafeCell 生命周期参数不能协变
}
#[repr(transparent)]
pub struct MyUnsafeCell<T: ?Sized> {
value: T,
}
fn test_my_cell<'long: 'short, 'short>(mut cell: MyUnsafeCell<&'short i32>, cell2: MyUnsafeCell<&'long i32>) {
cell = cell2; // 无错误
}
// 这么做是因为 我自己没有办法来处理生命周期,下面举了一个例子,在这里先简单提一嘴Cell
pub struct Cell<T: ?Sized> {
value: UnsafeCell<T>,
}
impl<T> Cell<T> {
pub fn set(&self, val: T) {
let old = self.replace(val);
drop(old);
}
pub fn replace(&self, val: T) -> T {
mem::replace(unsafe { &mut *self.value.get() }, val)
}
}
// Cell 通过UnsafeCell提供的get方法获取裸指针,再通过&mut *转换为可变引用,再调用mem::replace替换整个值来进行设置
// 举个例子,仿照UnsafeCell与Cell,定义自己的MyUnsafeCell与MyCell
use std::mem;
pub struct MyUnsafeCell<T: ?Sized> {
value: T,
}
impl<T> MyUnsafeCell<T> { // 自己定义的可以对生命周期参数进行协变
pub const fn new(value: T) -> MyUnsafeCell<T> {
MyUnsafeCell { value }
}
}
impl<T: ?Sized> MyUnsafeCell<T> {
pub const fn get(&self) -> *mut T {
self as *const MyUnsafeCell<T> as *const T as *mut T
}
}
pub struct MyCell<T: ?Sized> {
value: MyUnsafeCell<T>,
}
impl<T> MyCell<T> {
pub fn new(value: T) -> Self {
Self {
value: MyUnsafeCell::new(value),
}
}
pub fn set(&self, val: T) {
let _ = mem::replace(unsafe { &mut *self.value.get() }, val);
}
pub fn get(&self) -> T
where
T: Copy,
{
unsafe { *self.value.get() }
}
}
fn test_set<'long, 'short>(cell: &MyCell<&'long isize>, value: &'short isize)
where
'long: 'short,
{
let cell: &MyCell<&'short isize> = cell; // 此处发生了协变,长生命周期Cell 赋值给 短生命周期 Cell
cell.set(value);
}
fn main() {
let local = 1;
let cell = MyCell::new(&local);
{
let local2 = 21;
test_set(&cell, &local2);
}
let p = cell.get();
println!("{}", p);
}
这么做可以编译成功,但是local2已经离开作用域并被销毁,但是cell却还引用了local2,此时出现了悬空指针
将MyCell换成官方的Cell,编译将不会成功,
原因你应该已经知道,官方Cell使用了UnsafeCell,编译器对UnsafeCell做了特殊处理,让UnsafeCell不能对生命周期参数进行协变
UnsafeCell 小结:
1、UnsafeCell无额外开销
2、UnsafeCell实现了从不可变引用到可变裸指针的转换
3、编译器对UnsafeCell的生命周期泛型做了特殊处理
4、使用 UnsafeCell 去实现部可变性,而不是自己通过transmute等手段强转
三、Cell基本用法及内部机制
// 回到最初的例子,在满足借用规则的情况下 实现内部可变性?
// Cell 核心方法 已经提过一嘴
// 通过UnsafeCell提供的get方法获取裸指针,再通过&mut *转换为可变引用,再调用mem::replace替换整个值来进行设置
use std::cell::Cell;
struct XStruct<'a> {
x: &'a Cell<i32>,
}
fn add_and_print(x_struct: &XStruct) {
let x = x_struct.x.get(); // 可获得 基础数据的引用
x_struct.x.set(x + 1);
println!("The value of x_struct in add_and_print is {:?}.", x_struct); // 2
}
fn main() {
let cell_x = Cell::new(1);
let x_struct = XStruct { x: &cell_x };
add_and_print(&x_struct);
let x = cell_x.get(); // 唯一的区别是,我们首先必须从单元格中取出值,修改它,然后再修改它。
cell_x.set(x + 1);
println!("Final value of x_struct is {:?}.", x_struct); // 3
println!("Final value of x is {:?}.", cell_x); // 3
}
四、ReCell 基本用法及内部机制
// RefCell 提供 borrow方法进行不可变借用,borrow_mut方法进行可变借用
use std::cell::RefCell;
struct XStruct<'a> {
x: &'a RefCell<i32>,
}
fn add_and_print(x_struct: &XStruct) {
*x_struct.x.borrow_mut() += 1; // 可变的借用和取消引用基础数据
println!("The value of x_struct in add_and_print is {:?}.", x_struct); // 2
}
fn main() {
let ref_cell_x = RefCell::new(1); // ref_cell_x 是不可变的
let x_struct = XStruct { x: &ref_cell_x }; // 传递对 ref_cell_x 的不可变引用
add_and_print(&x_struct); // 传递对 x_struct 的不可变引用
*ref_cell_x.borrow_mut() += 1; // 修改 ref_cell_x 的值
println!("Final value of x_struct is {:?}.", x_struct);
println!("Final value of x is {:?}.", ref_cell_x);
}
RefCell VS Cell
不同之处,Cell是整个值做替换,而RefCell是引用着值
Cell 返回对基础数据的引用,来回复制数据会带来性能成本
通过共享struct,对某个值做修改;都用于单线程的共享引用,多线程的共享引用—Mutex
因为其内部运行时状态的实现不能保证跨线程安全
五、tips
此章节,可变性与C、C++的特性不一致需注意;
C++与Rust可变性、移动和所有权