使用 Drop Trait 运行清理代码

ch15-03-drop.md
commit d44317c3122b44fb713aba66cc295dee3453b24b

对于智能指针模式来说第二个重要的 trait 是 Drop,其允许我们在值要离开作用域时执行一些代码。可以为任何类型提供 Drop trait 的实现,同时所指定的代码被用于释放类似于文件或网络连接的资源。我们在智能指针上下文中讨论 Drop 是因为其功能几乎总是用于实现智能指针。例如,当 Box 被丢弃时会释放 box 指向的堆空间。

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

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

示例 15-14 展示了唯一定制功能就是当其实例离开作用域时,打印出 Dropping CustomSmartPointer! 的结构体 CustomSmartPointer。这会演示 Rust 何时运行 drop 函数:

文件名: src/main.rs

  1. struct CustomSmartPointer {
  2. data: String,
  3. }
  4. impl Drop for CustomSmartPointer {
  5. fn drop(&mut self) {
  6. println!("Dropping CustomSmartPointer with data `{}`!", self.data);
  7. }
  8. }
  9. fn main() {
  10. let c = CustomSmartPointer {
  11. data: String::from("my stuff"),
  12. };
  13. let d = CustomSmartPointer {
  14. data: String::from("other stuff"),
  15. };
  16. println!("CustomSmartPointers created.");
  17. }

示例 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 方法:

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

  1. $ cargo run
  2. Compiling drop-example v0.1.0 (file:///projects/drop-example)
  3. Finished dev [unoptimized + debuginfo] target(s) in 0.60s
  4. Running `target/debug/drop-example`
  5. CustomSmartPointers created.
  6. Dropping CustomSmartPointer with data `other stuff`!
  7. Dropping CustomSmartPointer with data `my stuff`!

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

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

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

如果我们像是示例 15-14 那样尝试调用 Drop trait 的 drop 方法,就会得到像示例 15-15 那样的编译错误:

文件名: src/main.rs

  1. struct CustomSmartPointer {
  2. data: String,
  3. }
  4. impl Drop for CustomSmartPointer {
  5. fn drop(&mut self) {
  6. println!("Dropping CustomSmartPointer with data `{}`!", self.data);
  7. }
  8. }
  9. fn main() {
  10. let c = CustomSmartPointer {
  11. data: String::from("some data"),
  12. };
  13. println!("CustomSmartPointer created.");
  14. c.drop();
  15. println!("CustomSmartPointer dropped before the end of main.");
  16. }

示例 15-15:尝试手动调用 Drop trait 的 drop 方法提早清理

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

  1. $ cargo run
  2. Compiling drop-example v0.1.0 (file:///projects/drop-example)
  3. error[E0040]: explicit use of destructor method
  4. --> src/main.rs:16:7
  5. |
  6. 16 | c.drop();
  7. | --^^^^--
  8. | | |
  9. | | explicit destructor calls not allowed
  10. | help: consider using `drop` function: `drop(c)`
  11. For more information about this error, try `rustc --explain E0040`.
  12. error: could not compile `drop-example` due to previous error

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

Rust 不允许我们显式调用 drop 因为 Rust 仍然会在 main 的结尾对值自动调用 drop,这会导致一个 double free 错误,因为 Rust 会尝试清理相同的值两次。

因为不能禁用当值离开作用域时自动插入的 drop,并且不能显式调用 drop,如果我们需要强制提早清理值,可以使用 std::mem::drop 函数。

std::mem::drop 函数不同于 Drop trait 中的 drop 方法。可以通过传递希望提早强制丢弃的值作为参数。std::mem::drop 位于 prelude,所以我们可以修改示例 15-15 中的 main 来调用 drop 函数。如示例 15-16 所示:

文件名: src/main.rs

  1. struct CustomSmartPointer {
  2. data: String,
  3. }
  4. impl Drop for CustomSmartPointer {
  5. fn drop(&mut self) {
  6. println!("Dropping CustomSmartPointer with data `{}`!", self.data);
  7. }
  8. }
  9. fn main() {
  10. let c = CustomSmartPointer {
  11. data: String::from("some data"),
  12. };
  13. println!("CustomSmartPointer created.");
  14. drop(c);
  15. println!("CustomSmartPointer dropped before the end of main.");
  16. }

示例 15-16: 在值离开作用域之前调用 std::mem::drop 显式清理

运行这段代码会打印出如下:

  1. $ cargo run
  2. Compiling drop-example v0.1.0 (file:///projects/drop-example)
  3. Finished dev [unoptimized + debuginfo] target(s) in 0.73s
  4. Running `target/debug/drop-example`
  5. CustomSmartPointer created.
  6. Dropping CustomSmartPointer with data `some data`!
  7. 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 和一些智能指针的特性,让我们聊聊标准库中定义的其他几种智能指针。