当一个值在Rust中被遮盖时,栈上会发生什么?

3
我正在阅读《精通Rust》一书。第一章末尾有一个练习,提供了示例代码,任务是通过使用通常很有帮助的编译器错误消息来修复它。
我本来以为以下内容是错误的,但实际上并不是。
for line in reader.lines() {
    let line = line.expect("Could not read line.");

为了完整的上下文,我有整个代码的gist。那是我修复后的代码,相关的行是37和38。但需要将一个文本文件作为参数输入。
我原本预计会出现一个错误,因为line在堆栈上(至少指针是)。它是否正确,即使没有投诉,它仍然可以被销毁和替换
关于内存管理和堆栈,底层发生了什么?我假设 line实际上是对字符串的引用(&str类型)。所以,这是可以的,因为无论哪种情况,指针本身 - 堆栈上的对象 - 只是一个usize,因此两个line对象在堆栈上的大小相同。
我可以使用不同大小的东西吗?第二行可以说:
let line: f64 = 3.42;

在这种情况下,对象本身位于栈上,并且它的大小可能大于usize

@Stargateur 我不认为这是一个完全的重复问题,因为我的根本问题不在于 为什么 这样做可以,而在于 如何 实现这个功能? 在栈内部,事情是如何发生的,使得这种功能成为可能的呢? 是否有两个不同的对象,一个是旧的'line',另一个是新的'line',即使旧的'line'已经无法访问了?或者旧的对象真的被覆盖了(如果在相同的范围内)? 如果是这样,不同大小的类型如何实现? - Mike Williamson
1个回答

4
每当使用let声明一个变量时,它就是一个全新的变量,与之前的任何内容都是分开的。即使已存在一个同名变量,原始变量在新变量处于作用域内时被shadowed(遮蔽),如果变量被遮蔽,则通常是无法访问的。
在旧变量仍然在作用域内而新变量超出作用范围之后或旧变量具有Drop实现的情况下,可以访问旧变量的值。
我们可以在以下示例中看到这一点。
#[derive(Debug)]
struct DroppedU32(u32);

impl Drop for DroppedU32 {
    fn drop(&mut self) {
        eprintln!("Dropping u32: {}", self.0);
    }
}

fn main() {
    let x = 5;
    dbg!(&x); // the original value
    {
        let x = 7;
        dbg!(&x); // the new value
    }
    dbg!(&x); // the original value again

    let y = DroppedU32(5);
    dbg!(&y); // the original value
    let y = DroppedU32(7);
    dbg!(&y); // the new value

    // down here, when the variables are dropped in
    // reverse order of declaration,
    // the original value is accessed again in the `Drop` impl.
}

(游乐场)

这并不意味着原变量一定仍然存在。编译器优化可能会导致原变量被覆盖,特别是如果原变量没有再次访问。

代码:

pub fn add_three(x: u32, y: u32, z: u32) -> u32 {
    let x = x + y;
    let x = x + z;
    x
}

编译成的结果

example::add_three:
        lea     eax, [rdi + rsi]
        add     eax, edx
        ret

如果你和我一样不太熟悉汇编代码,这段代码基本上是:
  1. 将x和y相加,并将结果存储在一个变量w中。
  2. 将z加到w中并用结果覆盖w。
  3. 返回w。
因此(除了输入参数),只使用了一个变量,即使我们两次使用了let x = ...。中间结果let x = x + y;被覆盖。

谢谢,@SCappella!这回答了我大部分的困惑,但现在我有另一个问题。我理解发生了什么逻辑上,但不知道在内存分配和Rust的0成本抽象中发生了什么。在我遮蔽但在同一作用域的情况下,就像你的第一个例子(let x=5; ... let x=7),因为它是相同的作用域,所以没有作用域可以再使用x=5。那么,在这种情况下,let x:i32 = 5 ... let x:u64 = 7会发生什么呢?我的问题是:我在堆栈上有一个32位的项目,现在在同一堆栈位置上有一个64位的项目,对吗?我该如何做到这一点? - Mike Williamson
1
@MikeWilliamson它们不占用相同的堆栈位置。原始变量变得无法访问,但它仍然存在于内存中未改变。我会添加一个示例来说明这一点。 - SCappella
3
最好的思考方式是将内部的 x 视为完全不同的变量,具有不同的名称。可以想象编译器甚至在内部将其重命名(或者行为就像它这样做)。这样很明显两个变量不共享相同的堆栈位置,它们具有不同的类型也没有问题。 - user4815162342
太好了,谢谢 @SCappella!顺便问一下,你如何获取字节码或它编译后的结果?在Python中,我使用dis库。Rust中是否有类似的东西?如果有,这是如何管理的,因为Rust编译成特定的架构/操作系统。同一程序在不同平台上会创建不同的字节码吗?还是字节码只是像JVM或Python中的中间码一样? - Mike Williamson
1
@MikeWilliamson 如果你想使用Cargo获取汇编代码,请参考这个问题。如果你愿意使用在线工具,请查看Compiler ExplorerRust Playground也有选项可以生成Rust使用的其他中间表示形式和汇编代码。对于你的其他问题,请尝试提出一个新问题,我相信你会得到一个好的答案。 - SCappella

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