27_rust_智能指针

智能指针

智能指针是一种数据结构,其行为与指针类似,有额外的元数据和功能。
引用计数(reference counting)智能指针类型,通过记录所有者的数量,使一份数据被多个所有者同时持有,并在没任何所有者时自动清理数据。
其中引用只借用数据,而智能指针常拥有所指向的数据。如智能指针String 和 Vec都拥有一片内存区域, 且允许用户对其操作 还拥有元数据 (例如容量等),可提供额外的功能或保障(String保障其数据是合法的UTF-8编码)。

智能指针的实现:

  • 智能指针通常使用 struct 实现, 并且实现了Deref和Drop这两个trait
  • Deref trait: 允许智能指针 struct 的实例像引用一样使用
  • Drop trait: 允许你自定义当智能指针实例走出作用域时的代码

标准库中常见的智能指针

  • Box:在heap内存上分配值
  • Rc:启用多重所有权的引用计数类型
  • Ref 和 RefMut,通过 RefCell访问:在运行时而不是编译时强制借用规则的类型

其他内容:

  • 内部可变模式(inferior mutability pattern):不可变类型暴露出可修改其内部值的 API
  • 引用循环(reference cycles) 它们如何泄露内存,以及如何防止其发生

使用Box<T>指向Heap上的数据

Box<T>是最简单的智能指针,允许在heap上存储数据,形式为在stack上有一指针类型,存放了指向Heap数据的地址,无性能开销,也无其它额外功能,但实现了Deref trait和Drop trait,所以是智能指针。
Box<T>的常用场景

  • 在编译时,某类型的大小无法确定,但使用该类型时,上下文却需要知道确切大小
  • 有大量数据,需要移交所有权,但需确保在操作时数据不会被复制
  • 在使用某个值时,只关心是否实现了特定的trait,而不关心具体类型
fn main() {
  let a = Box::new(3); // 3存在堆里
  println!("{}", a);
} // 至此a的生命周期结束,会自动释放stack的指针,以及释放heap的数据内存

使用Box赋能递归类型
在编译时,rust需要知道一个类型所占的内存空间大小,而递归类型的大小无法在编译时确定,但Box类型的大小确定,在递归类型中使用Box就可解决上述问题。在函数式语言中叫Cons List。
使用Box来获得确定大小的递归类型
Box<T>是一指针,指针本身的大小是固定的,指向的heap数据大小可不确定,这样编译时便可知需要多少内存。实际上就是c/c++语言中结构体和类中包含指针类型。
Box<T>

  • 只提供了“间接”存储和heap内存分配的功能
  • 没有其它额外功能也没有性能开销
  • 适用于需要“间接”存储的场景,如Cons List
  • 实现了Deref trait(使得能当引用处理) 和Drop trait(自动释放)
use crate::List::{Cons, Nil};
fn main() {
  let list = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil))))));
}
enum List {
  Cons(i32, Box<List>),
  Nil,
}

Deref Trait

实现Deref Trait,可自定义解引用运算符*的行为。通过实现Deref,智能指针可像常规引用一样来处理。
解引用运算符:(常规引用也是一种指针)

fn main() {
  let x = 3;
  let y = &x;
  println!("{}{}", x, *y);
}

上面代码要想获得y所指向的值,就需要加上*号解引用。
Box<T>代替上面代码中的引用:

fn main() {
  let x = 3;
  let y = Box::new(x);
  assert_eq!(3, x);
  assert_eq!(3, *y);
}

定义智能指针

Box<T>被定义成拥有一个元素的tuple struct。
实现Deref Trait:标准库中的Deref trait要求实现一个deref方法,该方法借用self,并返回一个指向内部数据的引用。

use std::ops::Deref;

struct TestBox<T>(T); // 定义tuple
impl<T> TestBox<T> {
    fn new(x: T) -> TestBox<T> {
      TestBox(x)
    }
}
impl<T> Deref for TestBox<T> {
    type Target = T; // 定义关联类型

    fn deref(&self) -> &T {
        &self.0 // 解引用,返回tuple第0个元素
    }
}

fn main() {
    let x = 5;
    let y = TestBox::new(x);
    println!("Hello, world! {}", *y); // *y相当于*(y.deref())
    assert_eq!(5, *y);
}

函数和方法的隐式解引用转化(Deref Coercion)

隐式解引用转化(Deref Coercion)是为函数和方法提供的一种便捷特性。

假设T实现了Deref trait,Deref Coercion 可以把T的引用转化为T经过Deref操作后生成的引用。

当把某类型的引用传递给函数或方法时,但它的类型与定义的参数类型不匹配,Deref Coercion 就会自动发生,编译器会对 deref进行一系列调用,来把它转为所需的参数类型,此过程在编译时完成,没有额外性能开销。

use std::ops::Deref;

struct TestBox<T>(T); // 定义tuple
impl<T> TestBox<T> {
    fn new(x: T) -> TestBox<T> {
      TestBox(x)
    }
}
impl<T> Deref for TestBox<T> {
    type Target = T; // 定义关联类型

    fn deref(&self) -> &T {
        &self.0 // 解引用,返回tuple第0个元素
    }
}

fn print_func(nm: &str) {
    println!("{}", nm);
}

fn main() {
    let y = TestBox::new(String::from("hello rust"));
    // 因为TestBox实现了Deref trait,会自动把TestBox的引用转换成string的引用
    // => deref &String,又因标准库中String也实现了Deref trait,、
    // 且返回值是&str,继续调用deref,最后与函数目标类型匹配。
    print_func(&y);
    // 如果没有Coercion功能,则需要按照如下方式传参
    print_func(&(*y)[..]); //首先解引用,再取字符串引用,进行切片
}

因为Coercion功能,编译阶段会不断尝试调用deref,使得与目标类型匹配。

解引用与可变性

可使用DerefMut trait 重载可变引用的 * 运算符。
在类型和 trait 在下列三种情况发生时, Rust 会执行 deref coercion:

  • 当 T: Deref<Target=U>, 允许 &T 转换为 &U
  • 当 T: DerefMut<Target=U>, 允许 &mut T 转换为 &mut U
  • 当 T: Deref<Target=U>, 允许 &mutT 转换为 &U,反过来不行

Drop trait

实现Drop trait,可让自定义当值将要离开作用域时发生的动作(类似于析构函数),比如文件、网络资源释放等,所有类型皆可实现Drop trait。

Drop trait只需实现drop方法,参数为对self的可变引用;Drop trait在预导入模块里(prelude),无需手动引入。

struct TestDrop {
    s: String,
}
impl Drop for TestDrop {
    fn drop(&mut self) {
        println!("drop func, {}", self.s);
    }
}
fn main() {
    let s1 = TestDrop { s: String::from("string 1") };
    let s2 = TestDrop { s: String::from("string 2") };
    println!("start drop");
}
/*输出结果,与析构过程一样,与变量定义顺序相反
start drop
drop func, string 2
drop func, string 1
*/

使用std::mem::drop来提前drop值

如果上面例子使用s1.drop()尝试手动调用drop方法,编译时会报错“explicit destructor calls not allowed”,无法手动调用析构函数。

Drop trait的目的是进行自动释放处理逻辑的资源,很难直接禁用drop功能,也没必要,rust也不允许手动调用Drop trait的drop方法。

但可调用标准库的std::mem::drop函数(也在prelude中),以提前drop值。且编译器能保证只调用一次drop,手动调用后不会再自动调用。

struct TestDrop {
    s: String,
}
impl Drop for TestDrop {
    fn drop(&mut self) {
        println!("drop func, {}", self.s);
    }
}
fn main() {
    let s1 = TestDrop { s: String::from("string 1") };
    drop(s1);
    let s2 = TestDrop { s: String::from("string 2") };
    println!("start drop");
}
/*输出结果
drop func, string 1
start drop
drop func, string 2
*/

Rc<T>引用计数智能指针

为了应付一个值被多处引用,拥有多个所有者的场景,rust引入了引用计数智能指针,类似于Java里的垃圾回收机制(引用计数法),使得rust能够支持多重所有权,Rc<T>

  • reference couting(引用计数)
  • 在实例内部维护一个记录引用次数的计数器,追踪所有到值的引用,从而判断值是否还在被使用
  • 如果计数器减少到0,也就是有0个引用时,该值可被安全清理,而不会发生引用失效的问题

Rc<T>使用场景:

  • 需要在heap上分配数据,且数据被程序的多个部分读取(只读),但编译时无法确定哪个部分最后使用完这些数据。(如果可确定哪个最后使用完,直接让其成为所有者即可)
  • Rc<T>只能用于单线程场景

Rc<T>的基本信息:

  • Rc<T>不知预导入模块(prelude)里
  • Rc::clone<&a>函数:增加引用计数
  • Rc::strong_count(&a):获得强引用计数,还有Rc::weak_count函数获得弱引用计数
enum ListTest {// 定义一个链表枚举
    Cons(i32, Box<ListTest>), // 变体Cons可存储数据,第一个是链表的数据,第二个则是指向下一个链表节点的引用
    Nil // 第二个变体是链表的结束值:空
}
use crate::ListTest::{ Cons, Nil};

fn main() {
    let a = Cons(5, Box::new(Cons(10, Box::new(Nil))));
    let b = Cons(3, Box::new(a));
    let c = Cons(6, Box::new(a)); //编译报错 use of moved value: `a`
}

修改使用Rc引用

enum ListTest {
    Cons(i32, Rc<ListTest>),
    Nil
}
use crate::ListTest::{ Cons, Nil}; // 引入枚举
use std::rc::Rc;

fn main() {
    let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
    // 如果a.clone()执行的是深拷贝
    let b = Cons(3, Rc::clone(&a)); // b不再获得所有权,只增加引用计数
    let c = Cons(6, Rc::clone(&a)); // rc clone仅增加引用计数,不会进行深拷贝
}

并可通过strong_count打印出强引用计数:

enum ListTest {
    Cons(i32, Rc<ListTest>),
    Nil
}
use crate::ListTest::{ Cons, Nil}; // 引入枚举
use std::rc::Rc;

fn main() {
    let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
    println!("a count after a ={}", Rc::strong_count(&a));
    let b = Cons(3, Rc::clone(&a));
    println!("a count after b ={}", Rc::strong_count(&a));
    {
    let c = Cons(6, Rc::clone(&a));
    println!("a count after c ={}", Rc::strong_count(&a));
    }
    println!("a count leave c scope ={}", Rc::strong_count(&a));
}
/*打印结果
a count after a =1
a count after b =2
a count after c =3
a count leave c scope =2
*/

可见每增加一次引用,计数加1,离开一个引用的作用域计数减少1。

Rc::clone()与类型的clone()方法的区别:

  • Rc::clone()增加引用,不会执行深拷贝
  • 类型的clone()很多会执行深拷贝

Rc<T>通过不可变引用,使得在程序不同部分之间共享只读数据。如果是可变引用的话,会因为多个可变引用改变数据,导致数据竞争不一致的问题。

RefCell<T>和内部可变性

内部可变性 (interior mutability) :
内部可变性是 Rust 的设计模式之一,它允许在只持有不可变引用的前提下对数据进行修改。数据结构中使用了 unsafe 代码来绕过 Rust 正常的可变性和借用规则。

RefCell<T>
Rc<T>不同,RefCell<T>类型代表其持有数据的唯一所有权。

借用规则:在任何给定时间内,要么只能拥有一个可变引用,要么只能拥有任意数量的不可变引用,引用总是有效的。

RefCell<T>Box<T>的区别:

RefCell<T> Box<T>
只会在运行时检查借用规则 编译阶段强制代码遵守借用规则
否则触发panic 否则出现错误

借用规则在不同阶段进行检查的比较:

编译阶段检查 运行时检查
尽早暴露问题 问题暴露延后,甚至到生产环境
无任何运行时开销 因借用计数产生少许性能损失
对大多数场景是最佳选择,是rust的默认行为 实现某些特定的内存安全场景(不可变环境中修改自身数据)

rust语言的编译器是非常保守的,即便有些代码可确定没问题,但写法上存在异常,编译器无法分析清楚,都不会让其通过,这样可保障代码可能繁琐些,但可保证运行时的安全性和稳定性,这也是rust语言的优势之一。
RefCell<T>Rc<T>相似,只能用于单线程场景。

选择Box<T>Rc<T>RefCell<T>的依据:

Box<T> Rc<T> RefCell<T>
同一个数据的所有者 一个 多个 一个
可变性、借用检查 可变、不可变借用(编译时检查) 不可变借用(编译时检查) 可变、不可变借用(运行时检查)

其中:即便RefCell<T>本身不可变,但仍能修改其存储的值。
内部可变性:可变的借用借用一个不可变的值

两个方法:

  • borrow方法:返回智能指针Ref<T>,实现了Deref trait
  • borrow_mut方法:返回智能指针RefMut<T>,实现了Deref trait

例子:使用struct实现mock obj的测试打桩功能(rust里未提供打桩测试功能,可通过struct来实现该功能)

// lib.rs代码
pub trait Msg {
    fn send(&self, msg: &str);
}
pub struct Limit<'a, T: 'a + Msg> {
    msg: &'a T,
    v: usize,
    max: usize,
}
impl<'a, T> Limit<'a, T>
where
    T: Msg,
{
    pub fn new(msg: &T, max: usize) -> Limit<T> {
        Limit { msg, v: 0, max, }
    }
    pub fn set_val(&mut  self, v: usize) {
        self.v = v;
        let m = self.v as f64 / self.max as f64;
        if m >= 1.0 {
            self.msg.send("Err: >= 1");
        } else if m >= 0.9 {
            self.msg.send("Warn: over 0.9");
        } else if m >= 0.7 {
            self.msg.send("Warn: over 0.7");
        }
    }
}
// 需要测试,传入不同value的时候,会发送不同msg,测试则通过存储发送的msg,检查是否符合预期
#[cfg(test)]
mod tests {
    use super::*;
    use std::cell::RefCell;
    struct MockMsg {
        sent_msg: RefCell<Vec<String>>,
    }
    impl MockMsg {
        fn new() -> MockMsg {
            MockMsg { sent_msg: RefCell::new(vec![]), }
        }
    }
    impl Msg for MockMsg {
        fn send(&self, message: &str) { // borrow_mut获得内部值的可变引用
            self.sent_msg.borrow_mut().push(String::from(message));
        }
    }

    #[test]
    fn test1() {
        let mmsg = MockMsg::new();
        let mut l = Limit::new(&mmsg, 100);
        l.set_val(80);
        assert_eq!(mmsg.sent_msg.borrow().len(), 1); //获得不可变引用
    }
}
//运行cargo test测试通过

RefCell<T>会记录当前存在多少个活跃的Ref<T>RefMut<T>智能指针:

  • 每次调用borrow,不可变借用计数加1
  • 任何一个Ref<T>的值离开作用域被释放时,不可变借用计数减1
  • 每次调用borrow_mut,可变借用计数加1
  • 任何一个RefMut<T>的值离开作用域被释放时,可变借用计数减1

维护借用检查的规则:

  • 任何一个给定时间里,只允许拥有多个不可变借用和一个可变借用,违背这个规则时将在运行时panic。

Rc<T>RefCell<T>结合使用,实现一个拥有多重所有权的可变数据

#[derive(Debug)]
enum ListTest {
    Cons(Rc<RefCell<i32>>, Rc<ListTest>),
    Nil
}
use crate::ListTest::{ Cons, Nil}; // 引入枚举
use std::rc::Rc;
use std::cell::RefCell;

fn main() {
    let v = Rc::new(RefCell::new(5));
    let a = Rc::new(Cons(Rc::clone(&v), Rc::new(Nil)));
    let b = Cons(Rc::new(RefCell::new(6)), Rc::clone(&a));
    let c = Cons(Rc::new(RefCell::new(7)), Rc::clone(&a));

    *v.borrow_mut() += 5;

    println!("a={:?},\nb={:?},\nc={:?}", a, b, c);
}
/*输出结果:
a=Cons(RefCell { value: 10 }, Nil),
b=Cons(RefCell { value: 6 }, Cons(RefCell { value: 10 }, Nil)),
c=Cons(RefCell { value: 7 }, Cons(RefCell { value: 10 }, Nil))
*/

其它可实现内部可变性的类型:
Cell<T>:通过赋值来访问数据
Mutex<T>:用于实现跨线程情形下的内部可变性模式

循环引用可导致内存泄漏

rust提供了可靠的安全保障,使得程序很难发生内存泄露。但如使用Rc<T>RefCell<T>创造出循环引用,而导致内存泄漏,因为每个项的引用数量不会变成0,值不会被释放。简单理解就是一个两个元素的循环链表,相互拥有指向对方的智能指针,使得计数不会减少到0。

use crate::ListTest::{Cons, Nil};
use std::cell::RefCell;
use std::rc::Rc;

#[derive(Debug)]
enum ListTest {
    Cons(i32, RefCell<Rc<ListTest>>), // 用于指向下一个元素
    Nil
}
impl ListTest {
    fn tail(&self) -> Option<&RefCell<Rc<ListTest>>> {
        match self { // 用于返回第二个元素
            Cons(_, item) => Some(item),
            Nil => None,
        }
    }
}

fn main() {
    let a = Rc::new(Cons(5, RefCell::new(Rc::new(Nil)))); // 第一个元素5,第二个元素nil
    println!("a rc cnt={}, a->next{:?}", Rc::strong_count(&a), a.tail());
    let b = Rc::new(Cons(10, RefCell::new(Rc::clone(&a)))); // 使得b指向a有个引用
    println!("a rc cnt={}, b cnt={}, b->next{:?}", Rc::strong_count(&a),
        Rc::strong_count(&b), b.tail());

    if let Some(l) = a.tail() { // 取出a的第二个元素
      *l.borrow_mut() = Rc::clone(&b); // 通过borrow mut将里边存储的Nil值改成b的引用
    }
    println!("a rc cnt={}, b cnt={}", Rc::strong_count(&a), Rc::strong_count(&b));
}
/*打印结果
a rc cnt=1, a->nextSome(RefCell { value: Nil })
a rc cnt=2, b cnt=1, b->nextSome(RefCell { value: Cons(5, RefCell { value: Nil }) })
a rc cnt=2, b cnt=2
*/

上述例子各自计数都为2,构成了一个双向链表:
image
上面的代码main函数走完时,首先会释放b,因为b是后创建的,会将b的引用计数减少到1,因为a中还有个指向b的引用,所以b在内存上并不会释放,a释放时也同理。

use crate::ListTest::{Cons, Nil};
use std::cell::RefCell;
use std::rc::Rc;

#[derive(Debug)]
enum ListTest {
    Cons(i32, RefCell<Rc<ListTest>>), // 用于指向下一个元素
    Nil
}
impl ListTest {
    fn tail(&self) -> Option<&RefCell<Rc<ListTest>>> {
        match self { // 用于返回第二个元素
            Cons(_, item) => Some(item),
            Nil => None,
        }
    }
}

fn main() {
    let a = Rc::new(Cons(5, RefCell::new(Rc::new(Nil)))); // 第一个元素5,第二个元素nil
    println!("a rc cnt={}, a->next{:?}", Rc::strong_count(&a), a.tail());
    let b = Rc::new(Cons(10, RefCell::new(Rc::clone(&a)))); // 使得b指向a有个引用
    println!("a rc cnt={}, b cnt={}, b->next{:?}", Rc::strong_count(&a),
        Rc::strong_count(&b), b.tail());

    if let Some(l) = a.tail() { // 取出a的第二个元素
      *l.borrow_mut() = Rc::clone(&b); // 通过borrow mut将里边存储的Nil值改成b的引用
    }
    println!("a rc cnt={}, b cnt={}", Rc::strong_count(&a), Rc::strong_count(&b));
    println!("a->next{:?}", a.tail()); // 这条将不断循环打印,直到溢出
}
/*输出:
....
ue: Cons(5,
thread 'main' has overflowed its stack
*/

增加一行打印后,可见堆栈溢出错误。

虽然上述例子是刻意构造出来的内存溢出,但也说明是存在此种可能的。
防止内存泄露的解决办法:

  • 依靠开发者保证,不能完全靠rust编译器。
  • 重新组织数据结构,一些引用来表达所有权,一些引用不表达所有权,循环引用中的一部分具有所有权关系,另一部分不涉及所有权关系,而只有所有权关系才影响值的清理。

为了防止循环引用,把Rc<T>换成Weak<T>

弱引用Weak<T>

Rc<T>换成Weak<T>,Rc::clone为Rc<T>实例的strong_count加1,Rc<T>的实例只有在strong_count为0的时候才会被清理。Rc<T>实例通过调用Rc::downgrade方法可创建值的Weak Reference(弱引用),返回类型是Weak<T>智能指针,调用Rc::downgrade会为weak_count加1.

  • Rc<T>使用weak_count来追踪存在多少Weak<T>
  • weak_count不为0并不影响Rc<T>实例的清理

强引用(Stong Reference)是关于如何分享Rc<T>实例的所有权,弱引用(Weak Reference)并不获得所有权。使用弱引用不会创建循环引用,因为当强引用计数为0时,弱引用会自动断开。在使用Weak<T>前,需要保证其指向的值仍然存在,可在Weak<T>实例上调用upgrade方法,返回Option<Rc<T>>确认是否还在。

例子:创建一个树,父节点通过next能找到子节点,子节点也能通过parent找到父节点,类似于循环引用

use std::rc::{Rc, Weak};
use std::cell::RefCell;

#[derive(Debug)]
struct Node { // 一个树节点的数据结构
    v: i32,
    parent: RefCell<Weak<Node>>, // 指向父节点是一个弱引用,防止之前的循环引用的问题
    next: RefCell<Vec<Rc<Node>>>, // 指向下一层(孩子)节点集合,树的结构,下一层可能有多个节点
}

fn main() {
    let level2 = Rc::new(Node { // 先创建子节点
        v: 6,
        parent: RefCell::new(Weak::new()), // 暂时先设成空引用
        next: RefCell::new(vec![]), // 下一层子节点集合是一个空的vector
    });
    // 先打印parent,当前应是None,先用borrow获得不可变引用,再用upgrade将Weak<T>转成Rc<T>
    println!("parent={:?}", level2.parent.borrow().upgrade());
    let level1 = Rc::new(Node { // 创建父节点,第一层
        v: 8,
        parent: RefCell::new(Weak::new()),
        next: RefCell::new(vec![Rc::clone(&level2)]), //下一层指向level2节点
    });
    // 让level2的parent指向上一层level1,获得可变引用,再通过downgrade将Rc<T>转成Weak<T>
    *level2.parent.borrow_mut() = Rc::downgrade(&level1);
    println!("parent={:?}", level2.parent.borrow().upgrade());
}
/*打印结果
parent=None
parent=Some(Node { v: 8, parent: RefCell { value: (Weak) }, next: RefCell { value: [Node { v: 6, parent: RefCell { value: (Weak) }, next: RefCell { value: [] } }] } })
*/

进一步打印出强引用和弱引用计数,可看出Weak count虽然为1,离开作用域依然可正常释放level1节点

use std::rc::{Rc, Weak};
use std::cell::RefCell;

#[derive(Debug)]
struct Node { // 一个树节点的数据结构
    v: i32,
    parent: RefCell<Weak<Node>>, // 指向父节点是一个弱引用,防止之前的循环引用的问题
    next: RefCell<Vec<Rc<Node>>>, // 指向下一层(孩子)节点集合,树的结构,下一层可能有多个节点
}

fn main() {
    let level2 = Rc::new(Node { // 先创建子节点
        v: 6,
        parent: RefCell::new(Weak::new()), // 暂时先设成空引用
        next: RefCell::new(vec![]), // 下一层子节点集合是一个空的vector
    });
    // 打印出level2的强引用和弱引用
    println!("level2 strong={}, weak={}", Rc::strong_count(&level2), Rc::weak_count(&level2));

    { // 创建出一个内部作用域
        let level1 = Rc::new(Node { // 创建父节点,第一层
            v: 8,
            parent: RefCell::new(Weak::new()),
            next: RefCell::new(vec![Rc::clone(&level2)]), //下一层指向level2节点,level2的强引用计数加1
        });
        // 让level2的parent指向上一层level1,获得可变引用,再通过downgrade将Rc<T>转成Weak<T>
        *level2.parent.borrow_mut() = Rc::downgrade(&level1);

        // 建立parent关系后,打印计数
        println!("level2 strong={}, weak={}", Rc::strong_count(&level2), Rc::weak_count(&level2));
        println!("level1 strong={}, weak={}", Rc::strong_count(&level1), Rc::weak_count(&level1));
    } // 离开这个作用域,level1强引用计数减到0,释放,level2的强引用计数也减1

    println!("level2 strong={}, weak={}", Rc::strong_count(&level2), Rc::weak_count(&level2));
    println!("parent={:?}", level2.parent.borrow().upgrade());
}

执行cargo run结果:

level2 strong=1, weak=0
level2 strong=2, weak=0 # 创建level1节点后,next指向level2,使其强引用计数+1
level1 strong=1, weak=1 # 此时因level2的parent指向level1,且为弱引用,所以weak count为1,
level2 strong=1, weak=0 # 离开作用域后,level1释放,level2的强引用计数也减1
parent=None

可见弱引用使用前需要判断是否为None,因为所指向的节点可能已经释放。

posted @ 2023-11-12 00:47  00lab  阅读(15)  评论(0编辑  收藏  举报