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

智能指针模式中重要的第二个 trait 是 Drop,它让你可以自定义值即将超出作用域时发生的情况。你可以为任何类型提供 Drop trait 的实现,这些代码可以用于释放文件或网络连接等资源。

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

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

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

列表 15-14 显示了一个 CustomSmartPointer 结构体,其唯一的自定义功能是在实例超出作用域时打印 Dropping CustomSmartPointer!,以显示 Rust 何时运行 drop 函数。

Filename: 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.");
}
Listing 15-14: A CustomSmartPointer struct that implements the Drop trait where we would put our cleanup code

Drop 特性包含在前言中,所以我们不需要将其引入作用域。我们在 CustomSmartPointer 上实现 Drop 特性,并为 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` 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,调用我们指定的代码。变量的销毁顺序与它们的创建顺序相反,因此 dc 之前被销毁。此示例的目的是为您提供一个关于 drop 方法如何工作的视觉指南;通常情况下,您会指定类型需要运行的清理代码,而不是打印消息。

使用 std::mem::drop 提前释放值

不幸的是,禁用自动 drop 功能并不简单。通常情况下,禁用 drop 并不必要;Drop 特性的整个目的是它会自动处理。然而,有时你可能希望提前清理一个值。一个例子是在使用管理锁的智能指针时:你可能希望强制调用释放锁的 drop 方法,以便同一作用域中的其他代码可以获取锁。Rust 不允许你手动调用 Drop 特性的 drop 方法;相反,如果你想在作用域结束前强制释放一个值,必须调用标准库提供的 std::mem::drop 函数。

如果我们尝试通过修改列表 15-14 中的 main 函数来手动调用 Drop 特性中的 drop 方法,如列表 15-15 所示,我们将得到一个编译器错误:

Filename: 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.");
}
Listing 15-15: Attempting to call the drop method from the Drop trait manually to clean up early

当我们尝试编译这段代码时,我们会得到这个错误:

$ 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。这会导致 双重释放 错误,因为 Rust 会尝试两次清理同一个值。

我们不能禁用值超出作用域时自动插入的drop,也不能显式调用drop方法。所以,如果我们需要强制提前清理一个值,我们使用std::mem::drop函数。

std::mem::drop 函数与 Drop 特性中的 drop 方法不同。我们通过传递想要强制丢弃的值作为参数来调用它。该函数在前言中,因此我们可以修改列表 15-15 中的 main 来调用 drop 函数,如列表 15-16 所示:

Filename: 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.");
}
Listing 15-16: Calling std::mem::drop to explicitly drop a value before it goes out of scope

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

$ 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 特性实现中指定的代码,使清理既方便又安全:例如,你可以用它来创建你自己的内存分配器!通过 Drop 特性和 Rust 的所有权系统,你不必记得清理,因为 Rust 会自动完成。

你也不必担心因意外清理仍在使用中的值而产生的问题:确保引用始终有效的所有权系统也确保当值不再被使用时,drop 只会被调用一次。

现在我们已经研究了Box<T> 和一些智能指针的特性,让我们来看看标准库中定义的其他几个智能指针。