使用 Drop Trait 在清理时运行代码

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

我们之所以在智能指针的上下文中介绍 Drop,是因为在实现智能指针时几乎总是会用到 Drop trait 的功能。例如,当一个 Box<T> 被 drop 时,它会释放 box 指向的堆上的空间。

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

你可以通过实现 Drop trait 来指定值超出作用域时要运行的代码。Drop trait 要求你实现一个名为 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` 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,调用我们指定的代码。变量按照创建的相反顺序 drop,因此 dc 之前 drop。此示例的目的是为你提供一个 drop 方法如何工作的可视化指南;通常,你将指定你的类型需要运行的清理代码,而不是打印消息。

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

不幸的是,禁用自动 drop 功能并不简单。禁用 drop 通常不是必需的;Drop trait 的重点在于它是自动处理的。但是,有时你可能希望提前清理值。一个例子是使用管理锁的智能指针:你可能希望强制执行释放锁的 drop 方法,以便同一作用域中的其他代码可以获取锁。Rust 不允许你手动调用 Drop trait 的 drop 方法;相反,如果你想强制在值超出其作用域之前将其 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。这将导致双重释放错误,因为 Rust 将尝试两次清理同一值。

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

std::mem::drop 函数与 Drop trait 中的 drop 方法不同。我们通过传递要强制 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 以在值超出作用域之前显式 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 方法代码在此时被调用以 drop c

你可以使用 Drop trait 实现中指定的代码以多种方式使清理方便且安全:例如,你可以使用它来创建自己的内存分配器!借助 Drop trait 和 Rust 的所有权系统,你不必记住清理,因为 Rust 会自动执行此操作。

你也不必担心因意外清理仍在使用的值而导致的问题:确保引用始终有效的所有权系统也确保 drop 仅在不再使用该值时才被调用一次。

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