Rust Rc+RefCell详解,实现二叉树

error[E0507]: cannot move out of dereference of Ref<‘_, xxx::TreeNode>

rust编程中出现这个编译报错问题,大概率是因为Rc/RefCell使用不正确。
希望通过本节的分享能够帮助读者学会在rust单一所有权机制下,正确使用Rc/RefCell来表达一颗二叉树。

Rc(Reference Counted)

Rc(Reference Counted):是Rust标准库中,用于处理引用计数的智能指针。用来突破单一所有权的限制。其基本操作是通过clone()增加引用计数。

// Reference Counted Rc会把对应的数据结构创建在堆上
// 堆上的数据才适合被用来在多个函数调用栈帧中共享
let i1 = Rc::new(1);
// 复制引用计数
let i2 = i1.clone();
let i3 = i1.clone();
// 现在引用计数值为3
println!("{}", Rc::strong_count(&i1)); //3
println!("{}", Rc::strong_count(&i2)); //3
println!("{}", Rc::strong_count(&i3)); //3

外部可变性

简单来说,mut关键字注明的可修改性就是外部可变性

我们前面学习了,Rust中提供了两种引用/借用类型:

&:只读引用,只读引用不允许修改被引用的对象。

&mut:可变引用,可变引用才有修改权限。

在编译阶段,Rust会检查,同一作用域内,对于某一个对象的引用,只允许存在两种情况:

要么只有一个活跃的可变引用,要么同时存在多个只读引用。

RefCell和内部可变性

RefCell,提供内部包装类型的内部可变性,用来突破mut变量才能被修改(外部可变性)的限制。

//Rc是线程内部以只读的方式共享数据
//RefCell提供内部可变性
let cell = RefCell::new(10);
{// 运行时会检查借用规则,所以这里必须加大括号
 // 将可写借用跟后面的只读借用隔离开来
    let mut mut_ref = cell.borrow_mut();
    *mut_ref += 1;
}
println!("{}", cell.borrow());//11

其中,

  • borrow()方法返回内部包装对象的只读引用。

  • borrow_mut()返回内部包装对象的可写引用。

代码在运行时,会动态检查RefCell对象的借用规则,以保证:在同一作用域内,要么只存在一个可写借用,要么只存在1到多个只读借用

rust表达二叉树

rust语言单一所有权机制的根本设计,带来了与生俱来的内存安全多线程安全等巨大好处。
然而,存在一些天生就需要“多个所有权”来描述的数据结构。比如有向无环图(DAG)。拿一个最简单的例子来看:图中有3个节点,节点A和B都指向节点C。

A -> C <- B

按理说,节点A和B都对C该有“同等的所有权”。
在Rust中,我们应当如何来描述这种DAG结构呢?
我们可能会想到,像Java语言一样,定义如下结构:

/// 下面这样定义行不通,编译报错:
/// recursive type `Node` has infinite size
/// recursive type has infinite sizerustc(E0072)
/// insert some indirection (e.g., a `Box`, `Rc`, or `&`) to make `Node` representable: `Box<`, `>`
struct Node {
    v: i32, 
    next: Node
}

事实上,这样是行不通的。

Rust中不加&(引用/借用)的类型都是实实在在的结构化数据。像上面这样定义,Node结构内部将无穷嵌套真正的数据,也就是说Node结构的size会无穷大。这当然是没法允许的。

Java中之所以可以,是因为class中的非primitive数据类型成员,都只是一个引用而已。

class Node {
    public int val;
    public Node next;//next只是一个引用
}
引用方式

接着,我们尝试改成引用的方式。前面一篇文章分析过为什么借用/引用需要生命周期标注,这里不再展开,直接给出带生命周期标注的结构定义。

struct Node<'a> {
    v: i32, 
    next: &'a Node<'a>
}

事实上,一个节点也可能不需要指向下一个节点(或者说next成员为None),比如上面例子中的节点C。所以,next成员需要用Option来包装一下。

Node1来命名这个结构,重新完整定义一下,并实现一些方法进行测试:

struct Node1<'a> {
    v: i32, 
    next: Option<&'a Node1<'a>>
}
impl<'a> Node1<'a> {
    // 静态的new方法,创建一个next成员为None的节点
    fn new(v: i32) -> Self {
        Self{v, next: None}
    }
    fn set_next(&mut self, next: &'a Node1<'a>) {
        self.next = Some(next);
    }
}
// 定义节点的打印格式
impl std::fmt::Debug for Node1<'_> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}, next:{:?}", self.v, self.next)
    }
}
let mut n1 = Node1::new(1);
let mut n2 = Node1::new(2);
let n3 = Node1::new(3);
n1.set_next(&n3);
n2.set_next(&n3);
//1, next:Some(3, next:None)
println!("{:?}", n1);
//2, next:Some(3, next:None)
println!("{:?}", n2);

虽然通过引用勉强实现了一个最简单的DAG,但n1节点的next和n2节点的next都只是n3的引用。
也就是说n3节点并不是DAG真正的一部分,n3必须在DAG以外别的什么地方保存起来,才能在DAG中进行引用。

Rc+RefCell

定义一个二叉树节点的结构:

#[derive(Debug, PartialEq, Eq)]
pub struct TreeNode {
    pub val: i32,
    pub left: Option<Rc<RefCell<TreeNode>>>,
    pub right: Option<Rc<RefCell<TreeNode>>>,
}
impl TreeNode {
    #[inline]
    pub fn new(val: i32) -> Self {
        TreeNode {
            val,
            left: None,
            right: None,
        }
    }
}

二叉树的中序遍历

//error[E0507]: cannot move out of dereference of `Ref<'_, xxx::TreeNode>`
pub fn inorder_traverse(root: Option<Rc<RefCell<TreeNode>>>) -> Vec<i32> {
    fn dfs(root: Option<Rc<RefCell<TreeNode>>>, list: &mut Vec<i32>) {
        if let Some(x) = root {
            // RefCell.borrow()不会转移所有权
            // x.borrow().left必须先clone()增加引用计数,
            // 否则会报error[E0507]错误
            dfs(x.borrow().left.clone(), list);
            list.push(x.borrow().val);
            dfs(x.borrow().right.clone(), list);
        }
    }
    let mut list: Vec<i32> = vec![];
    dfs(root, &mut list);
    list
}
#[test]
fn test_main() {
    let mut s1: TreeNode = TreeNode::new(2);
    let s2: TreeNode = TreeNode::new(1);
    let s3: TreeNode = TreeNode::new(3);
    s1.left = Some(Rc::new(RefCell::new(s2)));
    s1.right = Some(Rc::new(RefCell::new(s3)));
    let root = Some(Rc::new(RefCell::new(s1)));
    println!("{:?}", inorder_traverse(root));//[1, 2, 3]
}

力扣题目

接下来,可以通过力扣上面的一道简单题来进一步熟悉rust代码实现的二叉树操作:
力扣题目:965. 单值二叉树
参考代码:

use std::rc::Rc;
use std::cell::RefCell;
impl Solution {
    //error[E0507]: cannot move out of dereference of `Ref<'_, xxx::TreeNode>`
    pub fn is_unival_tree(root: Option<Rc<RefCell<TreeNode>>>) -> bool {
        fn is_val(root: Option<Rc<RefCell<TreeNode>>>, val: i32) -> bool {
            if let Some(x) = root {
                // RefCell.borrow()不会转移所有权
                // x.borrow().left不clone()的话,会报error[E0507]错误
                x.borrow().val == val && is_val(x.borrow().left.clone(), val)
                    && is_val(x.borrow().right.clone(), val)
            } else {
                true
            }
        }
        //unwrap()调用会转移所有权,所以先clone()
        let val = root.clone().unwrap().borrow().val;
        is_val(root, val)
    }
}
  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值