析构函数

在介绍堆时,我们提到你需要负责释放你分配的内存。在介绍借用检查器时,我们也说过,在Rust中你很少需要直接管理内存。

这两个陈述起初可能看似矛盾。让我们通过引入作用域析构函数来看看它们是如何结合在一起的。

作用域

变量的作用域是指Rust代码中该变量有效或存活的区域。

变量的作用域从其声明开始。当以下之一发生时结束:

  1. 变量被声明的代码块(即{}之间)结束
    fn main() {
       // `x` is not yet in scope here
       let y = "Hello".to_string();
       let x = "World".to_string(); // <-- x's scope starts here...
       let h = "!".to_string(); //   |
    } //  <-------------- ...and ends here
  2. 变量的所有权转移到其他人(例如,函数或其他变量)
    fn compute(t: String) {
       // Do something [...]
    }
    
    fn main() {
        let s = "Hello".to_string(); // <-- s's scope starts here...
                    //                    | 
        compute(s); // <------------------- ..and ends here
                    //   because `s` is moved into `compute`
    } 

析构函数

当一个值的所有者超出作用域时,Rust会调用其析构函数。析构函数试图清理该值使用的资源,尤其是它分配的内存。

你可以通过将值传递给std::mem::drop来手动调用析构函数。这就是为什么Rust开发者常会说某个值已经被丢弃,以此来表达一个值已经超出作用域且其析构函数已被调用。

可视化解构点

我们可以通过插入显式的drop调用来“拼写”出编译器为我们所做的操作。回到之前的例子:

fn main() {
   let y = "Hello".to_string();
   let x = "World".to_string();
   let h = "!".to_string();
}

这等同于:

fn main() {
   let y = "Hello".to_string();
   let x = "World".to_string();
   let h = "!".to_string();
   // Variables are dropped in reverse order of declaration
   drop(h);
   drop(x);
   drop(y);
}

来看第二个例子,其中s的所有权被转移到compute

fn compute(s: String) {
   // Do something [...]
}

fn main() {
   let s = "Hello".to_string();
   compute(s);
}

它等同于:

fn compute(t: String) {
    // Do something [...]
    drop(t); // <-- Assuming `t` wasn't dropped or moved 
             //     before this point, the compiler will call 
             //     `drop` here, when it goes out of scope
}

fn main() {
    let s = "Hello".to_string();
    compute(s);
}

注意区别:尽管在main中调用computes不再有效,但在main中并没有sdrop(s)。 当你将值的所有权转移到函数时,你也正在转移清理它的责任

这确保了一个值的析构函数至多被调用一次,设计上防止了双重释放漏洞

丢弃后的使用

如果你尝试在值被丢弃后使用它会发生什么?

#![allow(unused)]
fn main() {
let x = "Hello".to_string();
drop(x);
println!("{}", x);
}

如果你尝试编译这段代码,你会收到错误:

#![allow(unused)]
fn main() {
error[E0382]: use of moved value: `x`
 --> src/main.rs:4:20
  |
3 |     drop(x);
  |          - value moved here
4 |     println!("{}", x);
  |                    ^ value used here after move
}

Drop消耗掉它被调用的对象,意味着调用后该对象不再有效。因此,编译器会阻止你使用它,避免了释放后使用漏洞

引用的丢弃

如果变量包含引用会怎样? 例如:

#![allow(unused)]
fn main() {
let x = 42i32;
let y = &x;
drop(y);
}

当你调用drop(y)...什么也没发生。如果你真的尝试编译这段代码,你会收到警告:

warning: calls to `std::mem::drop` with a reference 
         instead of an owned value does nothing
 --> src/main.rs:4:5
  |
4 |     drop(y);
  |     ^^^^^-^
  |          |
  |          argument has type `&i32`
  |

这回到了我们之前所说:我们只想调用一次析构函数。你可能对同一个值有多个引用——如果我们中的一个超出作用域时就调用它们指向的值的析构函数,其他引用会怎么样? 它们会指向一个不再有效的内存位置:一个所谓的悬挂指针,是释放后使用漏洞的近亲。Rust的所有权制度从设计上排除了这类漏洞。

参考资料

  • 本节练习位于 exercises/03_ticket_v1/11_destructor
1

Rust不保证析构函数一定会执行。例如,如果你选择故意泄露内存,它们就不会执行。