【RUST_BASIC】Rust 智能指针

2021-11-21  本文已影响0人  ixiaolong

1 Box<T>

最简单直接的智能指针是 box,其类型是 Box<T>,其允许将一个值放在堆上,留在栈上的则是指向堆数据的指针,多用于如下场景:

使用 box 在堆上储存一个 i32

fn main() {
    let b = Box::new(5);
    println!("b = {}", b);
}

使用 box 在堆上储存递归数据结构:

enum List {
    Cons(i32, Box<List>),
    Nil,
}

use crate::List::{Cons, Nil};

fn main() {
    let list = Cons(1,
        Box::new(Cons(2,
            Box::new(Cons(3,
                Box::new(Nil))))));
}

Box<T> 类型实现了 Deref trait,允许 Box<T> 值被当作引用对待;其也实现了 Drop trait,离开作用域时指针所指向的堆数据也会被清除。

2 Deref trait

实现 Deref trait 允许重载解引用运算符(dereference operator)*

使用解引用运算符来跟踪 i32 值的引用:

fn main() {
    let x = 5;
    let y = &x;

    assert_eq!(5, x);
    assert_eq!(5, *y);
}

如果编写为 assert_eq!(5, y);,则会得到编译错误。

实现 *y 过程为 *(y.deref()),其中 deref()Deref trait中的方法。

Deref 强制转换(deref coercions)是 Rust 在函数或方法传参上的一种便利,其将实现了 Deref 的类型的引用转换为原始类型通过 Deref 所能够转换的类型的引用:

fn hello(name: &str) {
    println!("Hello, {}!", name);
}

fn main() {
    let m = MyBox::new(String::from("Rust"));
    hello(&m);
}

MyBox<T> 实现了 Deref trait,Rust 可以通过 deref 调用将 &MyBox<String> 变为 &String,标准库中提供了 String 上的 Deref trait 实现,其会返回字符串 slice,Rust 再次调用 deref&String 变为 &str,这就符合 hello 函数的定义了。

如果 Rust 没有 Deref 强制转换则必须编写的代码如下:

fn main() {
    let m = MyBox::new(String::from("Rust"));
    hello(&(*m)[..]);
}

Rust 在发现类型和 trait 实现满足三种情况时会进行 Deref 强制转换:

3 Drop trait

指定在值离开作用域时应该执行的代码的方式是实现 Drop trait,Drop trait 要求实现一个叫做 drop 的方法,它获取一个 self 的可变引用:

struct CustomSmartPointer {
    data: String,
}

impl Drop for CustomSmartPointer {
    fn drop(&mut self) {
        println!("Dropping CustomSmartPointer with data `{}`!", self.data);
    }
}

fn main() {
    let c = CustomSmartPointer { data: String::from("my stuff") };
    let d = CustomSmartPointer { data: String::from("other stuff") };
    println!("CustomSmartPointers created.");
}

Rust 并不允许我们主动调用 Drop trait 的 drop 方法,因为整个 Drop trait 存在的意义在于其是自动处理的,但是有时可能需要提早清理某个值,应该使用的是由标准库提供的 std::mem::drop

fn main() {
    let c = CustomSmartPointer { data: String::from("some data") };
    println!("CustomSmartPointer created.");
    drop(c);
    println!("CustomSmartPointer dropped before the end of main.");
}

4 Rc<T>

Rust 有一个叫做 Rc<T> 的类型,其名称为引用计数(reference counting)的缩写。引用计数意味着记录一个值引用的数量来知晓这个值是否仍在被使用,如果某个值有零个引用,就代表没有任何有效引用并可以被清理。

Rc<T> 用于当我们希望在堆上分配一些内存供程序的多个部分读取,而且无法在编译时确定程序的哪一部分会最后结束使用它的时候。如果确实知道哪部分是最后一个结束使用的话,就可以令其成为数据的所有者,正常的所有权规则就可以在编译时生效。

enum List {
    Cons(i32, Rc<List>),
    Nil,
}

use crate::List::{Cons, Nil};
use std::rc::Rc;

fn main() {
    let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
    let b = Cons(3, Rc::clone(&a));
    let c = Cons(4, Rc::clone(&a));
}

Rc::clone 的实现并不像大部分类型的 clone 实现那样对所有数据进行深拷贝,其只会增加引用计数,其值可以通过调用 Rc::strong_count 函数获得:

fn main() {
    let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
    println!("count after creating a = {}", Rc::strong_count(&a));
    let b = Cons(3, Rc::clone(&a));
    println!("count after creating b = {}", Rc::strong_count(&a));
    {
        let c = Cons(4, Rc::clone(&a));
        println!("count after creating c = {}", Rc::strong_count(&a));
    }
    println!("count after c goes out of scope = {}", Rc::strong_count(&a));
}

这段代码会打印出:

count after creating a = 1
count after creating b = 2
count after creating c = 3
count after c goes out of scope = 2

通过不可变引用, Rc<T> 允许在程序的多个部分之间只读地共享数据。

另外注意 Rc<T> 只能用于单线程场景。

5 RefCell<T>

内部可变性(Interior mutability)是 Rust 中的一个设计模式,允许即使在有不可变引用时也可以改变数据,这通常是借用规则所不允许的。为了改变数据,该模式在数据结构中使用 unsafe 代码来模糊 Rust 通常的可变性和借用规则。

RefCell<T> 代表其数据的唯一的所有权。对于引用和 Box<T>,借用规则的不可变性作用于编译时。对于 RefCell<T>,这些不可变性作用于运行时;对于引用,如果违反这些规则,会得到一个编译错误。而对于 RefCell<T>,如果违反这些规则程序会 panic 并退出。

类似于 Rc<T>RefCell<T> 只能用于单线程场景。

如下为选择 Box<T>Rc<T>RefCell<T> 的理由:

当创建不可变和可变引用时,我们分别使用 &&mut 语法。对于 RefCell<T> 来说,则是 borrowborrow_mut 方法,borrow 方法返回 Ref<T> 类型的智能指针,borrow_mut 方法返回 RefMut<T> 类型的智能指针。

RefCell<T> 记录当前有多少个活动的 Ref<T>RefMut<T> 智能指针。每次调用 borrowRefCell<T> 将活动的不可变借用计数加一,当 Ref<T> 值离开作用域时,不可变借用计数减一。就像编译时借用规则一样,RefCell<T> 在任何时候只允许有多个不可变借用或一个可变借用。

RefCell<T> 的一个常见用法是与 Rc<T> 结合,如果有一个储存了 RefCell<T>Rc<T> 的话,就可以得到有多个所有者并且可以修改的值了。

#[derive(Debug)]
enum List {
    Cons(Rc<RefCell<i32>>, Rc<List>),
    Nil,
}

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

fn main() {
    let value = Rc::new(RefCell::new(5));

    let a = Rc::new(Cons(Rc::clone(&value), Rc::new(Nil)));

    let b = Cons(Rc::new(RefCell::new(6)), Rc::clone(&a));
    let c = Cons(Rc::new(RefCell::new(10)), Rc::clone(&a));

    *value.borrow_mut() += 10;

    println!("a after = {:?}", a);
    println!("b after = {:?}", b);
    println!("c after = {:?}", c);
}

这里创建了一个 Rc<RefCell<i32>> 实例并储存在变量 value 中以便之后直接访问,接着在 a 中用包含 valueCons 成员创建了一个 List。需要克隆 value 以便 avalue 都能拥有其内部值 5 的所有权,而不是将所有权从 value 移动到 a 或者让 a 借用 value

将列表 a 封装进了 Rc<T> 这样当创建列表 bc 时,他们都可以引用 a。一旦创建了列表 abc,我们将 value 的值加 10。为此对 value 调用了 borrow_mut 方法返回 RefMut<T> 智能指针,可以对其使用解引用运算符并修改其内部值。

6 循环引用

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

#[derive(Debug)]
enum List {
    Cons(i32, RefCell<Rc<List>>),
    Nil,
}

impl List {
    fn tail(&self) -> Option<&RefCell<Rc<List>>> {
        match self {
            Cons(_, item) => Some(item),
            Nil => None,
        }
    }
}

fn main() {
    let a = Rc::new(Cons(5, RefCell::new(Rc::new(Nil))));

    println!("a initial rc count = {}", Rc::strong_count(&a));
    println!("a next item = {:?}", a.tail());

    let b = Rc::new(Cons(10, RefCell::new(Rc::clone(&a))));

    println!("a rc count after b creation = {}", Rc::strong_count(&a));
    println!("b initial rc count = {}", Rc::strong_count(&b));
    println!("b next item = {:?}", b.tail());

    if let Some(link) = a.tail() {
        *link.borrow_mut() = Rc::clone(&b);
    }

    println!("b rc count after changing a = {}", Rc::strong_count(&b));
    println!("a rc count after changing a = {}", Rc::strong_count(&a));

    // Uncomment the next line to see that we have a cycle;
    // it will overflow the stack
    // println!("a next item = {:?}", a.tail());
}

可以看到将列表 a 修改为指向 b 之后, ab 中的 Rc<List> 实例的引用计数都是 2。在 main 的结尾,Rust 首先丢弃变量 bbRc<List> 实例的引用计数减 1。然而,因为 a 仍然引用 b 中的 Rc<List>Rc<List> 的引用计数是 1 ,所以 b 中的 Rc<List> 在堆上的内存不会被丢弃。接下来 Rust 会丢弃 a,这同理会将 aRc<List> 实例的引用计数从 2 减为 1。这个实例的内存也不能被丢弃,因为其他的 Rc<List> 实例仍在引用它。这些列表的内存将永远保持未被回收的状态。

循环引用

可以通过调用 Rc::downgrade 并传递 Rc<T> 实例的引用来创建其值的弱引用(weak reference),得到 Weak<T> 类型的智能指针,将 weak_count 加1。Rc<T> 类型使用 weak_count 来记录其存在多少个 Weak<T> 引用,weak_count 无需计数为 0 就能使 Rc<T> 实例被清理。

参考

https://kaisery.github.io/trpl-zh-cn/ch15-00-smart-pointers.html

上一篇 下一篇

猜你喜欢

热点阅读