经检查的未初始化的内存

和 C 语言一样,Rust 中的所有堆栈变量都是未初始化的,直到为它们明确赋值。与 C 不同的是,Rust 静态地阻止你读取它们,直到你为它们赋值。

fn main() {
    let x: i32;
    println!("{}", x);
}
  |
3 |     println!("{}", x);
  |                    ^ 使用了没有初始化的 `x`

这基于一个基本的分支分析:每个分支都必须在第一次使用x之前给它赋值。有趣的是,如果每个分支恰好赋值一次,Rust 不要求变量是可变的,以执行延迟初始化。然而这个分析并没有利用常量分析或类似的东西。所以下述的代码是可以编译的:

fn main() {
    let x: i32;

    if true {
        x = 1;
    } else {
        x = 2;
    }

    println!("{}", x);
}

但这个不行:

fn main() {
    let x: i32;
    if true {
        x = 1;
    }
    println!("{}", x);
}
  |
6 |     println!("{}", x);
  |                    ^ 使用了可能没有初始化的 `x`

这个又可以了:

fn main() {
    let x: i32;
    if true {
        x = 1;
        println!("{}", x);
    }
    // 不需要担心还有没有初始化 x 的分支,
    // 因为我们实际上并没有在别的分支使用 x
}

当然,虽然分析不考虑实际值,但它对依赖关系和控制流有相对复杂的理解。例如,这样是可以编译通过的:


#![allow(unused)]
fn main() {
let x: i32;

loop {
    // Rust 不知道这个分支会被无条件执行,
    // 因为它依赖于实际值
    if true {
        // 但是它确实知道循环只会有一次,
        // 因为我们会无条件 break,
        // 所以 x 不需要是可变的
        x = 0;
        break;
    }
}
// Rust 知道如果没有执行 break 的话,代码不会运行到这里
// 所以一旦运行到这里,x 一定已经初始化了
println!("{}", x);
}

如果一个值从一个变量中移出,并且该值的类型不是 Copy,该变量在逻辑上就会变成未初始化。也就是说:

fn main() {
    let x = 0;
    let y = Box::new(0);
    let z1 = x; // x 仍然是有效的,因为 i32 可以 Copy
    let z2 = y; // 现在 y 逻辑上未初始化,因为 Box 不能 Copy
}

然而,在这个例子中重新给y赋值需要将y标记为可变,这样一个安全的 Rust 程序就可以观察到y的值发生了变化:

fn main() {
    let mut y = Box::new(0);
    let z = y; // 现在 y 逻辑上未初始化,因为 Box 不能 Copy
    y = Box::new(1); // 重新初始化 y
}

否则y就像是一个全新的变量。