Rust是否释放被覆盖变量的内存?

30

我在 Rust 书籍中看到,您可以使用相同的名称定义两个不同的变量:

let hello = "Hello";
let hello = "Goodbye";

println!("My variable hello contains: {}", hello);

这将打印出:

My variable hello contains: Goodbye

第一个“hello”会发生什么?它会被释放吗?我该如何访问它?

我知道给两个变量起相同的名称是不好的,但如果这种情况发生在我在100行之后声明它时,则可能会很麻烦。


6
  1. Rust甚至没有垃圾回收机制。
  2. 这应该在书中解释Shadowing
- E net4
1
我知道 Rust 没有垃圾回收,但它自己清理内存的事实在某种程度上就像是一种垃圾回收。是否有手动清理内存的方法?或者像 Shepmaster 提到的那样,只有当内存超出范围时才会被释放? - Alexander Luna
这并不完全是你在问题中所问的。 - E net4
2
我不明白为什么这个问题被踩了。《Rust Book》根本没有解释重叠变量的内存会发生什么。@Shepmaster的回答解释得非常好! - Logan Reed
2
@LoganReed如果您查看修订历史记录,您会发现OP最初询问Rust的“垃圾收集器”是否做了X或Y。向快速搜索会显示Rust没有垃圾回收,因此我猜测人们之所以下投票是由于这些原因的某种组合。如果您认为当前形式中的问题很好,则鼓励您点赞。 - Shepmaster
3个回答

40

Rust没有垃圾回收机制

Rust是否会释放被覆盖变量的内存?

是的,否则将会导致内存泄漏,这将是一个非常糟糕的设计决策。当变量被重新赋值时,内存将被释放:

struct Noisy;
impl Drop for Noisy {
    fn drop(&mut self) {
        eprintln!("Dropped")
    }
}

fn main() {
    eprintln!("0");
    let mut thing = Noisy;
    eprintln!("1");
    thing = Noisy;
    eprintln!("2");
}

0
1
Dropped
2
Dropped

第一个hello会发生什么

它被隐藏了

除了你无法再访问它之外,与变量引用的数据没有任何“特殊”之处。当变量超出范围时,它仍然会被删除:

struct Noisy;
impl Drop for Noisy {
    fn drop(&mut self) {
        eprintln!("Dropped")
    }
}

fn main() {
    eprintln!("0");
    let thing = Noisy;
    eprintln!("1");
    let thing = Noisy;
    eprintln!("2");
}

0
1
2
Dropped
Dropped

另请参阅:

我知道给两个变量起相同的名字是不好的。

这并不是“不好”,而是一种设计决策。我认为像这样使用遮蔽是一个坏主意:

let x = "Anna";
println!("User's name is {}", x);
let x = 42;
println!("The tax rate is {}", x);

我认为像这样使用阴影是合理的:

let name = String::from("  Vivian ");
let name = name.trim();
println!("User's name is {}", name);

参见:

但如果这是意外发生的,因为我在它之下声明了100行,那将是真正的痛苦。

不要编写太大的函数,以至于你会“意外”做某些事情。这适用于任何编程语言。

有没有手动清理内存的方法?

您可以调用 drop

eprintln!("0");
let thing = Noisy;
drop(thing);
eprintln!("1");
let thing = Noisy;
eprintln!("2");

0
Dropped
1
2
Dropped

然而,正如oli_obk - ker指出,变量占用的堆栈内存只有在函数退出时才会被释放,而变量所占用的资源则会被释放。
所有关于drop的讨论都需要展示其(非常复杂的)实现:
fn drop<T>(_: T) {}

如果我在其他函数之外的全局作用域中声明变量会发生什么?
全局变量永远不会被释放,即使你能创建它们。

我知道 Rust 没有垃圾回收,但它自己清理内存的事实在某种程度上就是一种垃圾回收。是否有手动清理内存的方法?如果我在其他函数之外的全局作用域中声明变量会怎样? - Alexander Luna
好的,当它超出范围时,它会再次释放内存。你知道是否有手动方式来做这件事,还是Rust自己决定? - Alexander Luna
9
释放内存存在一个根本问题:堆栈(按定义)只能从顶部(或底部,视角而定)进行修改。因此,您无法在中间“释放”内存。如果您有三个变量a、b和c,按顺序排列,您无法“释放” b,您只能停止使用它或重复使用其内存,但无法释放它。 - oli_obk

16

在涉及到drop顺序时,对于变量的阴影重新分配(覆盖)之间存在差异。

所有局部变量通常在作用域结束时按照声明的相反顺序被删除(请参见The Rust Programming LanguageDrop章节)。这包括被遮蔽的变量。可以通过将值包装在一个简单的包装结构中来轻松检查此内容,当该包装结构被删除时(就在值本身被删除之前),它会打印出一些东西:

use std::fmt::Debug;

struct NoisyDrop<T: Debug>(T);

impl<T: Debug> Drop for NoisyDrop<T> {
    fn drop(&mut self) {
        println!("dropping {:?}", self.0);
    }
}

fn main() {
    let hello = NoisyDrop("Hello");
    let hello = NoisyDrop("Goodbye");

    println!("My variable hello contains: {}", hello.0);
}

打印以下内容(playground):
My variable hello contains: Goodbye
dropping "Goodbye"
dropping "Hello"

这是因为作用域中的新let绑定不会覆盖先前的绑定,所以就好像你写了

    let hello1 = NoisyDrop("Hello");
    let hello2 = NoisyDrop("Goodbye");

    println!("My variable hello contains: {}", hello2.0);

请注意,这个行为与下面的代码(playground)表面上非常相似,但实际上是不同的:

fn main() {
    let mut hello = NoisyDrop("Hello");
    hello = NoisyDrop("Goodbye");

    println!("My variable hello contains: {}", hello.0);
}

这段代码不仅将值按相反顺序丢弃,而且在打印消息之前丢弃第一个值!这是因为当您将变量赋值给一个变量(而不是使用新变量隐藏它)时,原始值会在新值移入之前被丢弃。

我开始说局部变量在超出范围时“通常”会被删除。因为您可以将值移入和移出变量,所以有时候需要在运行时才能确定何时需要删除变量,此时编译器实际上插入代码来跟踪“活动状态”,并在必要时删除这些值,因此您不能通过覆盖值而意外地导致泄漏。(但是,仍然可能通过调用mem::forget或创建具有内部可变性的Rc-循环来安全地泄漏内存。)

另请参阅


1
这里有几点需要注意:
在您提供的程序中,在编译时,“Hello”字符串不会出现在二进制文件中。这可能是编译器进行的优化,因为第一个值没有被使用。
fn main(){
  let hello = "Hello xxxxxxxxxxxxxxxx"; // Added for searching more easily.
  let hello = "Goodbye";

  println!("My variable hello contains: {}", hello);
}

然后测试:
$ rustc  ./stackoverflow.rs

$ cat stackoverflow | grep "xxx"
# No results

$ cat stackoverflow | grep "Goodbye"
Binary file (standard input) matches

$ cat stackoverflow | grep "My variable hello contains"
Binary file (standard input) matches

请注意,如果打印第一个值,字符串确实出现在二进制中,这证明这是编译器优化,不存储未使用的值。
另一个需要考虑的问题是,分配给“hello”的两个值(即“Hello”和“Goodbye”)都具有“&str”类型。这是指向在编译后静态存储在二进制文件中的字符串的指针。动态生成的字符串的示例是当您从某些数据生成哈希时,例如MD5或SHA算法(生成的字符串在二进制文件中不存在)。
fn main(){
  // Added the type to make it more clear.
  let hello: &str = "Hello";
  let hello: &str = "Goodbye";

  // This is wrong (does not compile):
  // let hello: String = "Goodbye";

  println!("My variable hello contains: {}", hello);
}

这意味着变量只是指向静态内存中的位置。在运行时不会分配内存,也不会释放内存。即使上述提到的优化不存在(即省略未使用的字符串),只有hello指向的内存地址位置会改变,但内存仍然被静态字符串使用。
对于String类型,情况会有所不同,请参考其他答案。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接