Rust---内部可变性

摘要: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可变性、移动和所有权

参考:
Rust 中的 Cell、RefCell 和内部可变性

  • 9
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
要配置rust-analyzer的代码提示,你可以按照以下步骤进行操作: 1. 确保已经在你的项目中安装了rust-analyzer插件。你可以在VSCode的插件市场中搜索并安装"rust-analyzer"插件。 2. 打开VSCode的设置(可以通过菜单栏的"文件" -> "首选项" -> "设置"或者使用快捷键Ctrl + ,打开设置)。 3. 在设置页面的搜索框中输入"rust-analyzer",找到相关的设置选项。 4. 根据你的需求,配置下列常用的代码提示相关的设置: - "rust-analyzer.enable": 设置为true以启用rust-analyzer插件。 - "rust-analyzer.completion.enable": 设置为true以启用代码补全功能。 - "rust-analyzer.completion.addCallArgumentSnippets": 设置为true以自动添加函数调用时的参数提示。 - "rust-analyzer.completion.addCallParenthesis": 设置为true以自动添加函数调用时的括号。 - "rust-analyzer.completion.postfix.enable": 设置为true以启用后缀代码补全功能,例如`.if`、`.let`等。 - "rust-analyzer.hover.enable": 设置为true以启用悬停提示功能。 - "rust-analyzer.inlayHints.enable": 设置为true以启用内联提示功能。 5. 根据你的需求,可以进一步自定义配置rust-analyzer的代码提示行为。你可以在设置中找到更多相关的选项,并根据注释进行配置。 6. 保存设置,并重启VSCode使更改生效。 通过以上步骤,你可以根据自己的喜好和需求来配置rust-analyzer的代码提示功能。请注意,具体的配置选项可能会因rust-analyzer插件版本的不同而有所差异,请参考插件的官方文档或参考其它资源获取更多定制化的配置信息。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值