【Rust】——使用Drop Trait 运行清理代码和Rc<T> 引用计数智能指针

Y小夜 2024-08-22 17:05:02 阅读 51

💻博主现有专栏:

                C51单片机(STC89C516),c语言,c++,离散数学,算法设计与分析,数据结构,Python,Java基础,MySQL,linux,基于HTML5的网页设计及应用,Rust(官方文档重点总结),jQuery,前端vue.js,Javaweb开发,Python机器学习等

🥏主页链接:

                Y小夜-CSDN博客

目录

🎯使用Drop Trait 运行清理代码

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

🎯Rc 引用计数智能指针

🎃使用Rc共享数据

🎃克隆Rc会增加引用计数


🎯使用Drop Trait 运行清理代码

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

        我们在智能指针上下文中讨论 Drop 是因为其功能几乎总是用于实现智能指针。

        在其他一些语言中的某些类型,我们不得不记住在每次使用完那些类型的智能指针实例后调用清理内存或资源的代码。如果忘记的话,运行代码的系统可能会因为负荷过重而崩溃。在 Rust 中,可以指定每当值离开作用域时被执行的代码,编译器会自动插入这些代码。于是我们就不需要在程序中到处编写在实例结束时清理这些变量的代码 —— 而且还不会泄漏资源。

        指定在值离开作用域时应该执行的代码的方式是实现 Drop trait。Drop trait 要求实现一个叫做 drop 的方法,它获取一个 self 的可变引用。为了能够看出 Rust 何时调用 drop,让我们暂时使用 println! 语句实现 drop

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.");

}

   Drop trait 包含在 prelude 中,所以无需导入它。我们在 CustomSmartPointer 上实现了 Drop trait,并提供了一个调用 println! 的 drop 方法实现。drop 函数体是放置任何当类型实例离开作用域时期望运行的逻辑的地方。这里选择打印一些文本以可视化地展示 Rust 何时调用 drop

        在 main 中,我们新建了两个 CustomSmartPointer 实例并打印出了 CustomSmartPointer created.。在 main 的结尾,CustomSmartPointer 的实例会离开作用域,而 Rust 会调用放置于 drop 方法中的代码,打印出最后的信息。注意无需显式调用 drop 方法:

当运行这个程序,会出现如下输出:

$ cargo run

Compiling drop-example v0.1.0 (file:///projects/drop-example)

Finished dev [unoptimized + debuginfo] target(s) in 0.60s

Running `target/debug/drop-example`

CustomSmartPointers created.

Dropping CustomSmartPointer with data `other stuff`!

Dropping CustomSmartPointer with data `my stuff`!

        当实例离开作用域 Rust 会自动调用 drop,并调用我们指定的代码。变量以被创建时相反的顺序被丢弃,所以 d 在 c 之前被丢弃。这个例子的作用是给了我们一个 drop 方法如何工作的可视化指导,不过通常需要指定类型所需执行的清理代码而不是打印信息。

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

        不幸的是,我们并不能直截了当的禁用 drop 这个功能。通常也不需要禁用 drop ;整个 Drop trait 存在的意义在于其是自动处理的。然而,有时你可能需要提早清理某个值。一个例子是当使用智能指针管理锁时;你可能希望强制运行 drop 方法来释放锁以便作用域中的其他代码可以获取锁。Rust 并不允许我们主动调用 Drop trait 的 drop 方法;当我们希望在作用域结束之前就强制释放变量的话,我们应该使用的是由标准库提供的 std::mem::drop

fn main() {

let c = CustomSmartPointer {

data: String::from("some data"),

};

println!("CustomSmartPointer created.");

c.drop();

println!("CustomSmartPointer dropped before the end of main.");

}

如果尝试编译代码会得到如下错误:

$ cargo run

Compiling drop-example v0.1.0 (file:///projects/drop-example)

error[E0040]: explicit use of destructor method

--> src/main.rs:16:7

|

16 | c.drop();

| --^^^^--

| | |

| | explicit destructor calls not allowed

| help: consider using `drop` function: `drop(c)`

For more information about this error, try `rustc --explain E0040`.

error: could not compile `drop-example` due to previous error

        错误信息表明不允许显式调用 drop。错误信息使用了术语 析构函数destructor),这是一个清理实例的函数的通用编程概念。析构函数 对应创建实例的 构造函数。Rust 中的 drop 函数就是这么一个析构函数。

        Drop trait 实现中指定的代码可以用于许多方面,来使得清理变得方便和安全:比如可以用其创建我们自己的内存分配器!通过 Drop trait 和 Rust 所有权系统,你无需担心之后的代码清理,Rust 会自动考虑这些问题。

        我们也无需担心意外的清理掉仍在使用的值,这会造成编译器错误:所有权系统确保引用总是有效的,也会确保 drop 只会在值不再被使用时被调用一次。

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

        大部分情况下所有权是非常明确的:可以准确地知道哪个变量拥有某个值。然而,有些情况单个值可能会有多个所有者。例如,在图数据结构中,多个边可能指向相同的节点,而这个节点从概念上讲为所有指向它的边所拥有。节点在没有任何边指向它从而没有任何所有者之前,都不应该被清理掉。

        为了启用多所有权需要显式地使用 Rust 类型 Rc<T>,其为 引用计数reference counting)的缩写。引用计数意味着记录一个值的引用数量来知晓这个值是否仍在被使用。如果某个值有零个引用,就代表没有任何有效引用并可以被清理。

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

🎃使用Rc<T>共享数据

        可以改变 Cons 的定义来存放一个引用,不过接着必须指定生命周期参数。通过指定生命周期参数,表明列表中的每一个元素都至少与列表本身存在的一样久。这是示例 15-17 中元素与列表的情况,但并不是所有情况都如此。

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

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));

}

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

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

🎃克隆Rc<T>会增加引用计数

        以便观察创建和丢弃 a 中 Rc<List> 的引用时引用计数的变化。

        这样就可以观察当 c 离开作用域时引用计数如何变化。

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));

}

        在程序中每个引用计数变化的点,会打印出引用计数,其值可以通过调用 Rc::strong_count 函数获得。这个函数叫做 strong_count 而不是 count 是因为 Rc<T> 也有 weak_count

        从这个例子我们所不能看到的是,在 main 的结尾当 b 然后是 a 离开作用域时,此处计数会是 0,同时 Rc<List> 被完全清理。使用 Rc<T> 允许一个值有多个所有者,引用计数则确保只要任何所有者依然存在其值也保持有效。



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。