【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::clone
,Rc<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::clone
。Rc::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>
允许一个值有多个所有者,引用计数则确保只要任何所有者依然存在其值也保持有效。
上一篇: 【C++ 秘籍】解锁 stack、queue 和 priority_queue 及容器适配器的神奇世界
下一篇: 华为OD题库最新E卷题库大全【A卷+B卷+C卷+D卷+E卷】真题目录2024年8月更新校准【当前使用2024年E卷题库】
本文标签
声明
本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。