Rust中的Rc. Cell, RefCell

引用计数Rc

概述: Rc是Rust中用于实现引用计数的类型,它允许多个所有者共享同一个数据。

用法详解:

  • 每当clone一个Rc时,引用计数增加,而每当一个Rc退出作用域时,引用计数减少。
  • 当引用计数变为0时,Rc和它所包裹的数据都会被销毁。
  • Rc的clone不会进行深拷贝,指创建另一个指向包裹值的指针,并增加引用计数

示例:

rust 复制代码
use std::rc::Rc;

fn main() {
    let rc_examples = "Rc examples".to_string();
    {
        println!("rc_a is created");
        
        let rc_a: Rc<String> = Rc::new(rc_examples);
        println!("Reference Count of rc_a: {}", Rc::strong_count(&rc_a)); 

        {
            println!("rc_a is cloned to rc_b");

            let rc_b: Rc<String> = Rc::clone(&rc_a);
            println!("Reference Count of rc_b: {}", Rc::strong_count(&rc_b));
            println!("Reference Count of rc_a: {}", Rc::strong_count(&rc_a));

            println!("rc_a and rc_b are equal: {}", rc_a.eq(&rc_b));
            // 可以直接使用值的方法
            println!("Length of the value inside rc_a: {}", rc_a.len());
            // 直接使用值
            println!("Value of rc_b: {}", rc_b);

            println!("rc_b is dropped out of scope");
        }

        println!("Reference Count of rc_a: {}", Rc::strong_count(&rc_a));

        println!("rc_a is dropped out of scope");
    }
}

Cell和RefCell

概述: Rust编译器通过严格的借用规则(多个不可变引用或只有一个可变引用存在)确保程序安全性,但是会降低灵活性。因此提供了Cell和RefCell类型,允许在不可变引用的情况下修改数据。内部是通过unsafe代码实现的

Cell

概述: Cell和RefCell在功能上没有区别,区别在于Cell<T>适用于T实现Copy的情况

示例:

rust 复制代码
use std::cell::Cell;

fn main() {
    let c = Cell::new("asdf");
    let one = c.get();
    c.set("qwer");
    let two = c.get();
    println!("{}, {}", one, two);
}

asdf是&str类型,实现了Copy trait,取到值保存在one变量后,还能同时进行修改,这个违背了Rust的借用规则,但通过Cell就能做到这一点

rust 复制代码
let c = Cell::new(String::from("asdf"));

这段代码编译器会报错,因为String没有实现Copy trait

RefCell

Rust规则 智能指针带来的额外规则
一个数据只有一个所有者 Rc/Arc让一个数据可以拥有多个所有者
要么多个不可变借用,要么一个可变借用 RefCell实现编译器可变、不可变引用共存
违背规则导致编译错误 违背规则导致运行时panic
rust 复制代码
use std::cell::RefCell;

fn main() {
    let s = RefCell::new(String::from("hello, wolrd"));
    let s1 = s.borrow();
    let s2 = s.borrow_mut();
    
    println!("{},{}", s1, s2);
}

上面这段代码不会出现编译错误,但是运行时会panic

RefCell为何存在?

从上面看,通过RefCell并不能绕过rust的借用规则,那还有什么用?

对于大型的复杂程序,可以选择使用RefCell来让事情简化。在Rust编译器的ctxt结构体中有大量的RefCell类型的map字段,主要原因是这些map会被分散在各个地方的代码片段所广泛使用或修改,很容易就碰到编译器跑出来的各种错误,但是你不知道如何解决。这时候可以使用RefCell,在运行时发现这些错误,因为一旦有的代码使用不正确,就会panic,我们就知道哪里借用冲突了。

Cell or RefCell

主要区别:

  • Cell只适用于实现了Copy trait类型,用于提供值,而RefCell用于提供引用
  • Cell不会panic,而RefCell会在运行时panic

性能比较:

Cell没有额外的开销,下面两段代码的性能是一样的

rust 复制代码
// code 1
let x = Cell::new(1);
let y = &x;
let z = &x;
x.set(2);
y.set(3);
z.set(4);
println!("{}", x.get());

// code 2
let mut x = 2;
let y = &mut x;
let z = &mut x;
x = 2;
*y = 3;
*z = 4;
println!("{}", x);

但是代码2不能编译成功,因为只能存在一个可变引用

内部可变性

概述: 对一个不可变的值进行可变借用,就是内部可变性

无内部可变性:

rust 复制代码
fn main() {
    let x = 5;
    let y = &mut x;
}

尝试对一个不可变值进行可变借用,破坏了Rust的借用规则

RefCell应用场景:

rust 复制代码
// 定义在外部库的trait
pub trait Messnger {
    fn send(&self, msg: String);
}

// 我们自己写的代码
struct MsgQueue {
    msg_cache: Vec<String>,
}

impl Messnger for MsgQueue {
    fn send(&self, msg: String) {
        self.msg_cache.push(msg);
    }
}

上面代码会编译错误,因为需要修改self的msg_cache,但是外部库的self是不可变的self,这时候RefCell就派上用场了。

rust 复制代码
use std::cell::RefCell;

pub trait Messnger {
	fn send(&self, msg: String);
}

pub struct MsgQueue {
    msg_cache: RefCell<Vec<String>>>,
}

impl Messnger for MsgQueue {
    fn send(&self, msg: String) {
        self.msg_cache.borrow_mut().push(msg);
    }
}

fn main() {
    let mq = MsgQueue {
        msg_cache: RefCell::new(Vec::new());
    };
    mq.send("hello, world".to_string());
}

Rc+RefCell

概述: 这是一个很常见的组合,前者可以实现一个数据拥有多个所有者,后者可以实现数据的内部可变性

示例:

rust 复制代码
use std::cell::RefCell;
use std::rc::Rc;

fn main() {
    let s = Rc::new(RefCell::new("Hello, wolrd".to_string()));
    
    let s1 = s.clone();
    let s2 = s.clone();
    
    s2.borrow_mut().push_str(", on yeah!");
    
    println!("{:?}\n{:?}\n{:?}", s, s1, s2);
}

性能损耗:

非常高,大致相当于没有线程安全版本的C++ std::shared_ptr指针。C++这个指针的主要开销也在于原子性这个并发原语上,毕竟线程安全在哪个语言开销都不小

内存损耗:

二者结合的数据结构与下面类似:

rust 复制代码
struct Wrapper<T> {
    // Rc
    strong_count: usize,
    weak_count: usize,
    
    // RefCell
    borrow_count: isize,
    
    // 包裹的数据
    item: T,
}

仅仅多分配了三个usize/isize

CPU损耗:

从CPU来看, 损耗如下:

  • 对Rc<T>解引用是免费的(编译期),但是*带来的间接取值并不免费
  • clone Rc<T>需要将当前的引用计数跟0和usize::Max进行一次比较,然后将计数值加1
  • drop Rc<T>需要将计数值减1,然后跟0进行一次比较
  • 对RefCell进行不可变借用,需要将isize类型的借用计数加1,然后跟0进行比较
  • 对RefCell的不可变借用进行释放,需要将isize减1
  • 对RefCell的可变借用大致跟上面差不多,但需要先跟0比较,然后再减1
  • 对RefCell的可变借用进行释放,需要将isize加1

解决借用冲突

两种方法:

  • Cell::from_mut,将&mut T转换为Cell<T>
  • Cell::as_slice_of_cells,将&Cell<T>转换为&[Cell<T>]

常见的借用冲突问题:

rust 复制代码
fn is_even(i: i32) -> bool {
    i % 2 == 0
}

fn retain_even(nums: &mut Vec<i32>) {
    let mut i = 0;
    for num in nums.iter().filter(|&num| is_even(num)) {
        nums[i] = *num;
        i += 1;
    }
    nums.truncate(i);
}

会编译错误,因为同时使用了可变借用和不可变借用

可以通过索引来解决这个问题:

rust 复制代码
fn retain_even(nums: &mut Vec<i32>) {
    let mut i = 0;
    for j in 0..nums.len() {
        if is_even(nums[j]) {
            nums[i] = nums[j];
            i += 1;
        }
    }
    nums.truncate(i);
}

但这样不够最佳实践,使用迭代器才是最佳实践

可以使用上面提到的两种方法:

rust 复制代码
use std::cell::Cell;

fn retain_even(nums: &mut Vec<i32>) {
    let slice: &[Cell<i32>] = Cell::from_mut(&mut nums[..]).as_slice_of_cells();
    
    let mut i = 0;
    for num in slice.iter().filter(|num| is_even(num.get())) {
        slice[i].set(num.get());
        i += 1;
    }
    
    nums.truncate(i);
}
相关推荐
DongLi013 天前
rustlings 学习笔记 -- exercises/05_vecs
rust
番茄灭世神3 天前
Rust学习笔记第2篇
rust·编程语言
shimly1234563 天前
(done) 速通 rustlings(20) 错误处理1 --- 不涉及Traits
rust
shimly1234563 天前
(done) 速通 rustlings(19) Option
rust
@atweiwei3 天前
rust所有权机制详解
开发语言·数据结构·后端·rust·内存·所有权
shimly1234563 天前
(done) 速通 rustlings(24) 错误处理2 --- 涉及Traits
rust
shimly1234564 天前
(done) 速通 rustlings(23) 特性 Traits
rust
shimly1234564 天前
(done) 速通 rustlings(17) 哈希表
rust
shimly1234564 天前
(done) 速通 rustlings(15) 字符串
rust
shimly1234564 天前
(done) 速通 rustlings(22) 泛型
rust