Skip to content

智能指针-2

使用 Drop Trait 运行清理代码

对于智能指针模式来说第二个重要的 trait 是 Drop,其允许我们在值要离开作用域时执行一些代码。可以为任何类型提供 Drop trait 的实现,同时所指定的代码被用于释放类似于文件或网络连接的资源。

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

rust
struct CustomSmartPointer {
    data: String,
}
//Drop trait 包含在 prelude 中,所以无需导入它。
impl Drop for CustomSmartPointer {
    fn drop(&mut self) {//drop 函数体是放置任何当类型实例离开作用域时期望运行的逻辑的地方。
        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"),
    };
    //当实例离开作用域 Rust 会自动调用 drop,并调用我们指定的代码。变量以被创建时相反的顺序被丢弃,所以 d 在 c 之前被丢弃。
    println!("CustomSmartPointers created.");
}

通过 std::mem::drop 提早丢弃值

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

Rc 引用计数智能指针

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

注意 Rc<T> 只能用于单线程场景;

使用 Rc 共享数据

rust
/*
每一个 Cons 变量都包含一个值和一个指向 List 的 Rc<T>。当创建 b 时,不同于获取 a 的所有权,这里会克隆 a 所包含的 Rc<List>,这会将引用计数从 1 增加到 2 并允许 a 和 b 共享 Rc<List> 中数据的所有权。创建 c 时也会克隆 a,这会将引用计数从 2 增加为 3。每次调用 Rc::clone,Rc<List> 中数据的引用计数都会增加,直到有零个引用之前其数据都不会被清理。
*/
enum List {
    Cons(i32, Rc<List>),
    Nil,
}

use crate::List::{Cons, Nil};
/*
需要使用 use 语句将 Rc<T> 引入作用域,因为它不在 prelude 中。在 main 中创建了存放 5 和 10 的列表并将其存放在 a 的新的 Rc<List> 中。接着当创建 b 和 c 时,调用 Rc::clone 函数并传递 a 中 Rc<List> 的引用作为参数。

也可以调用 a.clone() 而不是 Rc::clone(&a),不过在这里 Rust 的习惯是使用 Rc::clone。Rc::clone 的实现并不像大部分类型的 clone 实现那样对所有数据进行深拷贝。Rc::clone 只会增加引用计数,这并不会花费多少时间。深拷贝可能会花费很长时间。通过使用 Rc::clone 进行引用计数,可以明显的区别深拷贝类的克隆和增加引用计数类的克隆。当查找代码中的性能问题时,只需考虑深拷贝类的克隆而无需考虑 Rc::clone 调用。
*/
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 会增加引用计数

rust
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)))));
    println!("count after creating a = {}", Rc::strong_count(&a)); //count after creating a = 1
    let b = Cons(3, Rc::clone(&a));
    println!("count after creating b = {}", Rc::strong_count(&a)); //count after creating b = 2
    {
        let c = Cons(4, Rc::clone(&a));
        println!("count after creating c = {}", Rc::strong_count(&a)); //count after creating c = 3
    }
    println!("count after c goes out of scope = {}", Rc::strong_count(&a));//count after c goes out of scope = 2
}
/*
a 中 Rc<List> 的初始引用计数为 1,接着每次调用 clone,计数会增加 1。当 c 离开作用域时,计数减 1。不必像调用 Rc::clone 增加引用计数那样调用一个函数来减少计数;Drop trait 的实现当 Rc<T> 值离开作用域时自动减少引用计数。
*/

RefCell 和内部可变性模式]

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

当可以确保代码在运行时会遵守借用规则,即使编译器不能保证的情况,可以选择使用那些运用内部可变性模式的类型。所涉及的 unsafe 代码将被封装进安全的 API 中,而外部类型仍然是不可变的。

通过 RefCell 在运行时检查借用规则

RefCell<T> 代表其数据的唯一的所有权。

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

在编译时检查借用规则的优势是这些错误将在开发过程的早期被捕获,同时对运行时没有性能影响,因为所有的分析都提前完成了。为此,在编译时检查借用规则是大部分情况的最佳选择,这也正是其为何是 Rust 的默认行为。

相反在运行时检查借用规则的好处则是允许出现特定内存安全的场景,而它们在编译时检查中是不允许的。静态分析,正如 Rust 编译器,是天生保守的

因为一些分析是不可能的,如果 Rust 编译器不能通过所有权规则编译,它可能会拒绝一个正确的程序;从这种角度考虑它是保守的。如果 Rust 接受不正确的程序,那么用户也就不会相信 Rust 所做的保证了。然而,如果 Rust 拒绝正确的程序,虽然会给程序员带来不便,但不会带来灾难。RefCell<T> 正是用于当你确信代码遵守借用规则,而编译器不能理解和确定的时候。

类似于 Rc<T>RefCell<T> 只能用于单线程场景。如果尝试在多线程上下文中使用RefCell<T>,会得到一个编译错误。

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

  • Rc<T> 允许相同数据有多个所有者;Box<T>RefCell<T> 有单一所有者。
  • Box<T> 允许在编译时执行不可变或可变借用检查;Rc<T>仅允许在编译时执行不可变借用检查;RefCell<T> 允许在运行时执行不可变或可变借用检查。
  • 因为 RefCell<T> 允许在运行时执行可变借用检查,所以我们可以在即便 RefCell<T> 自身是不可变的情况下修改其内部的值。

结合 RcRefCell 来拥有多个可变数据所有者

rust
//`RefCell<T>` 的一个常见用法是与 `Rc<T>` 结合。
#[derive(Debug)]
enum List {
    Cons(Rc<RefCell<i32>>, Rc<List>),//通过在 Cons 定义中使用 RefCell<T>,我们就允许修改所有列表中的值
    Nil,
}

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

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(3)), Rc::clone(&a));
    let c = Cons(Rc::new(RefCell::new(4)), Rc::clone(&a));

    *value.borrow_mut() += 10;

    println!("a after = {a:?}"); //a after = Cons(RefCell { value: 15 }, Nil)
    println!("b after = {b:?}"); //b after = Cons(RefCell { value: 3 }, Cons(RefCell { value: 15 }, Nil))
    println!("c after = {c:?}"); //c after = Cons(RefCell { value: 4 }, Cons(RefCell { value: 15 }, Nil))
}

引用循环与内存泄漏

Rust 的内存安全性保证使其难以意外地制造永远也不会被清理的内存(被称为 内存泄漏memory leak)),但并不是不可能。Rust 并不保证完全防止内存泄漏,这意味着内存泄漏在 Rust 中被认为是内存安全的。这一点可以通过 Rc<T>RefCell<T> 看出:创建引用循环的可能性是存在的。这会造成内存泄漏,因为每一项的引用计数永远也到不了 0,持有的数据也就永远不会被释放。

制造引用循环

rust
/*
这里在变量 a 中创建了一个 Rc<List> 实例来存放初值为 5, Nil 的 List 值。接着在变量 b 中创建了存放包含值 10 和指向列表 a 的 List 的另一个 Rc<List> 实例。

最后,修改 a 使其指向 b 而不是 Nil,这就创建了一个循环。为此需要使用 tail 方法获取 a 中 RefCell<Rc<List>> 的引用,并放入变量 link 中。接着使用 RefCell<Rc<List>> 的 borrow_mut 方法将其值从存放 Nil 的 Rc<List> 修改为 b 中的 Rc<List>。
如果保持最后的 println! 行注释并运行代码,会得到如下输出:
-----------------------------------------------------------
a initial rc count = 1
a next item = Some(RefCell { value: Nil })
a rc count after b creation = 2
b initial rc count = 1
b next item = Some(RefCell { value: Cons(5, RefCell { value: Nil }) })
b rc count after changing a = 2
a rc count after changing a = 2
------------------------------------------------------------
可以看到将列表 a 修改为指向 b 之后, a 和 b 中的 Rc<List> 实例的引用计数都是 2。在 main 的结尾,Rust 丢弃 b,这会使 b Rc<List> 实例的引用计数从 2 减为 1。然而,b Rc<List> 不能被回收,因为其引用计数是 1 而不是 0。接下来 Rust 会丢弃 a 将 a Rc<List> 实例的引用计数从 2 减为 1。这个实例也不能被回收,因为 b Rc<List> 实例依然引用它,所以其引用计数是 1。这些列表的内存将永远保持未被回收的状态。
*/
use crate::List::{Cons, Nil};
use std::cell::RefCell;
use std::rc::Rc;

#[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());
}