使用 Drop
Trait 运行清理代码
对于智能指针模式来说第二个重要的 trait 是 Drop
,其允许我们在值要离开作用域时自定义要执行的操作。可以为任何类型提供 Drop
trait 的实现,同时所指定的代码被用于释放类似于文件或网络连接的资源。
我们在智能指针上下文中讨论 Drop
,是因为在实现智能指针时几乎总会用到 Drop
trait。例如,当 Box<T>
被丢弃时会释放 box 指向的堆空间。
在其他一些语言中的某些类型,我们不得不记住在每次使用完那些类型的智能指针实例后调用清理内存或资源的代码。常见示例包括文件句柄(file handles)、套接字(sockets)和锁(locks)。如果忘记的话,运行代码的系统可能会因为负荷过重而崩溃。在 Rust 中,可以指定每当值离开作用域时被执行的代码,编译器会自动插入这些代码。于是我们就不需要在程序中到处编写在实例结束时清理这些变量的代码 —— 而且还不会泄漏资源!
指定在值离开作用域时应该执行的代码的方式是实现 Drop
trait。Drop
trait 要求实现一个叫做 drop
的方法,它获取一个 self
的可变引用。为了能够看出 Rust 何时调用 drop
,让我们暂时使用 println!
语句实现 drop
。
示例 15-14 展示了唯一定制功能就是当其实例离开作用域时,打印出 Dropping CustomSmartPointer!
的结构体 CustomSmartPointer
,这会演示 Rust 何时运行 drop
方法:
文件名:src/main.rs
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.");
}
示例 15-14:结构体 CustomSmartPointer
,其实现了放置清理代码的 Drop
trait
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` profile [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 方法如何工作的可视化指导,不过通常需要指定类型所需执行的清理代码而不是打印信息。
不幸的是,禁用自动 drop
功能并不是一件容易的事。通常也不需要禁用 drop
;整个 Drop
trait 存在的意义在于其是自动处理的。然而,有时你可能需要提早清理某个值。一个例子是当使用智能指针管理锁时;你可能希望强制运行 drop
方法来释放锁以便作用域中的其他代码可以获取锁。Rust 并不允许我们主动调用 Drop
trait 的 drop
方法;当我们希望在作用域结束之前就强制释放变量的话,我们应该使用的是由标准库提供的 std::mem::drop
函数。
如果我们像是示例 15-14 那样尝试调用 Drop
trait 的 drop
方法,就会得到像示例 15-15 那样的编译错误:
文件名:src/main.rs
示例 15-15:尝试手动调用 Drop
trait 的 drop
方法提早清理
如果尝试编译代码会得到如下错误:
$ 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
|
16 | drop(c);
| +++++ ~
For more information about this error, try `rustc --explain E0040`.
error: could not compile `drop-example` (bin "drop-example") due to 1 previous error
错误信息表明不允许显式调用 drop
。错误信息使用了术语析构函数(destructor),这是一个清理实例的函数的通用编程术语。析构函数 对应创建实例的构造函数(constructor)。Rust 中的 drop
函数就是这么一个析构函数。
Rust 不允许我们显式调用 drop
因为 Rust 仍然会在 main
的结尾对值自动调用 drop
,这会导致一个 double free 错误,因为 Rust 会尝试清理相同的值两次。
因为不能禁用当值离开作用域时自动插入的 drop
,并且不能显式调用 drop
方法。如果我们需要强制提早清理值,可以使用 std::mem::drop
函数。
std::mem::drop
函数不同于 Drop
trait 中的 drop
方法。可以通过传递希望强制丢弃的值作为参数。该函数位于 prelude,所以我们可以修改示例 15-15 中的 main
来调用 drop
函数。如示例 15-16 所示:
文件名:src/main.rs
示例 15-16: 在值离开作用域之前调用 std::mem::drop
显式清理
运行这段代码会打印出如下:
$ cargo run
Compiling drop-example v0.1.0 (file:///projects/drop-example)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.73s
Running `target/debug/drop-example`
CustomSmartPointer created.
Dropping CustomSmartPointer with data `some data`!
CustomSmartPointer dropped before the end of main.
Dropping CustomSmartPointer with data `some data`!
出现在 CustomSmartPointer created.
和 CustomSmartPointer dropped before the end of main.
之间,表明了 drop
方法被调用了并在此丢弃了 c
。
Drop
trait 实现中指定的代码可以用于多种场景,来使得清理变得方便和安全:比如可以用其创建我们自己的内存分配器!通过 Drop
trait 和 Rust 所有权系统,你无需担心之后的代码清理,因为 Rust 会自动完成这些工作。
你也不必担心由于不小心清理仍在使用的值而导致的问题:所有权系统确保引用总是有效的,也会确保 drop
只会在值不再被使用时被调用一次。
现在我们学习了 Box<T>
和一些智能指针的特性,让我们聊聊标准库中定义的其他几种智能指针。