1.7 Drop

原文链接: https://rust-unofficial.github.io/too-many-lists/first-drop.html

我们能造栈、压入、弹出,甚至还测过一切正常!

需要担心清理链表吗?技术上,完全不用!和 C++ 一样,Rust 用析构函数在资源用完时自动清理。若类型实现了名为 Drop 的 trait,它就有析构函数。Trait 是 Rust 对接口的 fancy 说法。Drop trait 的接口如下:

1
2
3
pub trait Drop {
    fn drop(&mut self);
}

基本上就是:「当你离开作用域时,我给你一点时间收拾善后」。

若你包含的类型已实现 Drop,而你要做的只是调用它们的析构函数,其实不必自己实现 Drop。对 List 来说,它只想 drop 自己的 head,head 又可能会尝试 drop 一个 Box<Node>。这些都会自动处理……但有一个坑。

自动处理会很糟。

考虑一条简单链表:

1
list -> A -> B -> C

当 list 被 drop 时,会尝试 drop A,A 会尝试 drop B,B 会尝试 drop C。有些人可能已经开始紧张了。这是递归代码,递归可能爆栈!

有些人会想:「这显然是尾递归,任何像样的语言都会保证这种代码不会爆栈」。事实上,这是错的!要明白为什么,我们试着写编译器必须做的事,像编译器那样手动为 List 实现 Drop:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
impl Drop for List {
    fn drop(&mut self) {
        // 注意:在真正的 Rust 代码里不能显式调用 `drop`;
        // 我们是在假装自己是编译器!
        self.head.drop(); // 尾递归 — 好!
    }
}

impl Drop for Link {
    fn drop(&mut self) {
        match *self {
            Link::Empty => {} // 完成!
            Link::More(ref mut boxed_node) => {
                boxed_node.drop(); // 尾递归 — 好!
            }
        }
    }
}

impl Drop for Box<Node> {
    fn drop(&mut self) {
        self.ptr.drop(); // 糟了,不是尾递归!
        deallocate(self.ptr);
    }
}

impl Drop for Node {
    fn drop(&mut self) {
        self.next.drop();
    }
}

我们不能在释放内存之后再 drop Box 的内容,所以没法用尾递归方式 drop!只能手动为 List 写迭代式 drop,把节点从它们的 box 里 hoist 出来。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
impl Drop for List {
    fn drop(&mut self) {
        let mut cur_link = mem::replace(&mut self.head, Link::Empty);
        // `while let` == 「直到这个模式不匹配就一直做这件事」
        while let Link::More(mut boxed_node) = cur_link {
            cur_link = mem::replace(&mut boxed_node.next, Link::Empty);
            // boxed_node 在此离开作用域并被 drop;
            // 但它的 Node 的 `next` 字段已设为 Link::Empty
            // 所以不会发生无界递归。
        }
    }
}
1
2
3
4
5
6
7
8
> cargo test

     Running target/debug/lists-5c71138492ad4b4a

running 1 test
test first::test::basics ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured

太好了!


Bonus

过早优化的附加章节!

我们的 drop 实现其实和 while let Some(_) = self.pop() { } 非常像,后者当然更简单。有什么不同?一旦我们把链表泛化到存整数以外的东西,可能带来哪些性能问题?

点击展开答案

pop 返回 Option<i32>,而我们的实现只操作 Link(Box<Node>)。所以我们的实现只移动指向节点的指针,而基于 pop 的实现会移动节点里存的值。若有人用链表存 VeryBigThingWithADropImpl(VBTWADI)的实例,泛化后这可能非常昂贵。Box 能在原地运行其内容的 drop 实现,所以不受此问题影响。VBTWADI 恰恰是让人想用链表而不是数组的那类东西,在这种情况下表现差会有点失望。

若想两全其美,可以加一个新方法 fn pop_node(&mut self) -> Link,pop 和 drop 都能从中干净地推导出来。

最后修改 August 23, 2026: 更新 (499855b16)