18、Destructors(析构函数)

Destructors(析构函数)

既然使用了堆,那么我们应该对这一块内存负责。C/C++中,我们需要手动调用free函数,清理不再使用的内存空间。Java,C#等语言,提供了GC来帮我们管理内存,但是会有性能损失。同样的,Rust的语言特性决定了他也要像C/C++那样,手动管理内存,这在编译时就要决定好,不过好在,我们不需要手动调用各种函数去管理内存,Rust为我们做好了自动析构,释放内存的机制。

管理堆内存+无需手动管理,其实可以通过引入作用域析构函数来实现。

作用域

作用域,就是一个变量有效的代码区域。

变量的作用域从它被声明的地方开始,当发生以下情况时,作用域结束:

  • 声明变量的块结束

    fn main() {
       // x还不在作用域里
       let y = "Hello".to_string();
       let x = "World".to_string(); // x的作用域开始
       let h = "!".to_string();
    } //  x的作用域结束
    

     

  • 变量的所有权被转移走

    fn compute(t: String) {
       // Do something [...]
    }
    
    fn main() {
        let s = "Hello".to_string(); // <-- s作用域开始
                    //                    | 
        compute(s); // <------------------- ..s作用域结束
                    //   因为s的所有权移动到了compute()里面的t
    }
    

析构函数

当值的所有者超出他自己的作用域时,Rust会为其调用析构函数。

析构函数会清理为这个值分配的任何资源,尤其是给它分配的内存。

我们可以通过将值传递给std::mem::drop来手动调用值的析构函数,以清理它所占用的资源。

这就是我们经常听到Rust开发人员说的:“that value has been dropped!”,这表示当前超出了值的作用域,或者是已经对它调用了析构函数。

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);
}

 

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中调用compute之后,我们将所有权转移给了compute里面的t,与此同时,t的drop调用也归compute管。因为所有权归哪个作用域,哪个作用域就管理这个值的drop。main就不会再调用s的drop了,为了避免二次drop的情况。

在drop之后使用值

如果我们在某个值被drop后仍然尝试使用它,就会发生错误

let x = "Hello".to_string();
drop(x);
println!("{}", x);

image-20250907192442722

drop会消耗被调用的值,这意味着该值在调用后不再有效。因此,编译器阻止我们使用它,避免释放后发生use-after-free bugs.

drop引用

在调用drop时,如果变量包含引用怎么办?

let x = 42i32;
let y = &x;
drop(y);

当我们调用drop(y)什么也没发生。但是如果我们尝试编译代码,就会收到警告

image-20250907192920205

 

这就是我们之前说的:“我们只想调用一次析构函数”。

我们可以对相同的值有多个引用。如果其中一个引用超出作用域,我们为它所指向的值调用析构函数,其他的引用将会指向一个不再有效的内存位置,即dangling pointer(悬垂指针),和use-after-free很相似。Rust所有权机制在设计上就排除了这种错误

阅读剩余
THE END