经检查的未初始化的内存

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

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

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

  1. fn main() {
  2. let x: i32;
  3. if true {
  4. x = 1;
  5. } else {
  6. x = 2;
  7. }
  8. println!("{}", x);
  9. }

但这个不行:

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

这个又可以了:

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

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

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

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

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

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

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

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