使用 Drop 特征在清理时运行代码

对智能指针模式很重要的第二个特征是 Drop,它允许您自定义值即将超出作用域时发生的操作。您可以为任何类型提供 Drop 特征的实现,并且该代码可用于释放资源,例如文件或网络连接。

我们在智能指针的上下文中介绍 Drop,因为 Drop 特征的功能几乎总是在实现智能指针时使用。例如,当 Box<T> 被丢弃时,它将释放该盒子指向的堆上的空间。

在某些语言中,对于某些类型,程序员必须在每次使用完这些类型的实例后调用代码来释放内存或资源。例子包括文件句柄、套接字或锁。如果他们忘记了,系统可能会过载并崩溃。在 Rust 中,您可以指定在值超出作用域时运行特定的代码段,编译器将自动插入此代码。因此,您无需在程序中使用特定类型实例的每个地方都小心地放置清理代码——您仍然不会泄漏资源!

您可以通过实现 Drop 特征来指定在值超出作用域时运行的代码。Drop 特征要求您实现一个名为 drop 的方法,该方法接受对 self 的可变引用。要查看 Rust 何时调用 drop,让我们暂时使用 println! 语句来实现 drop

清单 15-14 显示了一个 CustomSmartPointer 结构体,其唯一的自定义功能是当实例超出作用域时,它将打印 Dropping 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:一个实现了 Drop trait 的 CustomSmartPointer 结构体,我们可以在其中放置清理代码

Drop trait 包含在 prelude 中,因此我们不需要将其引入作用域。我们在 CustomSmartPointer 上实现 Drop trait,并为 drop 方法提供一个调用 println! 的实现。drop 函数的主体是你希望在类型的实例超出作用域时运行的任何逻辑。我们在这里打印一些文本,以直观地演示 Rust 何时调用 drop

main 函数中,我们创建了两个 CustomSmartPointer 的实例,然后打印 CustomSmartPointers 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,执行了我们指定的代码。变量按其创建顺序的反序被丢弃,因此 dc 之前被丢弃。此示例的目的是让你直观地了解 drop 方法的工作原理;通常,你会指定你的类型需要运行的清理代码,而不是打印消息。

使用 std::mem::drop 提前丢弃值

不幸的是,禁用自动 drop 功能并不容易。禁用 drop 通常是不必要的;Drop trait 的全部意义在于它是自动处理的。然而,有时你可能希望提前清理一个值。一个例子是使用管理锁的智能指针:你可能希望强制执行释放锁的 drop 方法,以便同一作用域中的其他代码可以获取锁。Rust 不允许你手动调用 Drop trait 的 drop 方法;相反,如果你想强制在值超出其作用域之前将其丢弃,则必须调用标准库提供的 std::mem::drop 函数。

如果我们尝试通过修改代码清单 15-14 中的 main 函数来手动调用 Drop trait 的 drop 方法,如代码清单 15-15 所示,我们将收到一个编译错误

文件名: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("some data"),
    };
    println!("CustomSmartPointer created.");
    c.drop();
    println!("CustomSmartPointer dropped before the end of main.");
}

代码清单 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。错误消息使用了术语“析构函数”,它是清理实例的函数的通用编程术语。“析构函数”类似于“构造函数”,后者用于创建实例。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

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("some data"),
    };
    println!("CustomSmartPointer created.");
    drop(c);
    println!("CustomSmartPointer dropped before the end of main.");
}

代码清单 15-16:调用 std::mem::drop 在值超出作用域之前显式丢弃它

运行此代码将打印以下内容

$ cargo run
   Compiling drop-example v0.1.0 (file:///projects/drop-example)
    Finished dev [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> 和智能指针的一些特性,让我们看看标准库中定义的其他一些智能指针。