理解Rust的变量作用域

Rust的所有权系统和作用域息息相关,因此有必要先理解Rust的作用域规则。

在Rust中,任何一个可用来包含代码的大括号都是一个单独的作用域。类似于Struct{}这样用来定义数据类型的大括号,不在该讨论范围之内,本文后面所说的大括号也都不考虑这种大括号。

包括且不限于以下几种结构中的大括号都有自己的作用域:

  • if、while等流程控制语句中的大括号
  • match模式匹配的大括号
  • 单独的大括号
  • 函数定义的大括号
  • mod定义模块的大括号

例如,可以单独使用一个大括号来开启一个作用域:

#![allow(unused)]
fn main() {
{                    // s 在这里无效, 它尚未声明
  let s = "hello";   // 从此处起,s是有效的
  println!("{}", s); // 使用 s
}                    // 此作用域已结束,s不再有效
}

上面的代码中,变量s绑定了字符串字面值,在跳出作用域后,变量s失效,变量s所绑定的值会自动被销毁。

注:上文【变量s绑定的值会被销毁】的说法是错误的

实际上,变量跳出作用域失效时,会自动调用Drop Trait的drop函数来销毁该变量绑定在内存中的数据,这里特指销毁堆和栈上的数据,而字符串字面量是存放在全局内存中的,它会在程序启动到程序终止期间一直存在,不会被销毁。可通过如下代码验证:

fn main(){
    {
      let s = "hello";
      println!("{:p}", s);  // 0x7ff6ce0cd3f8
    }

    let s = "hello";
    println!("{:p}", s);  // 0x7ff6ce0cd3f8
}

因此,上面的示例中只是让变量s失效了,仅此而已,并没有销毁s所绑定的字符串字面量。

但一般情况下不考虑这些细节,而是照常描述为【跳出作用域时,会自动销毁变量所绑定的值】。

任意大括号之间都可以嵌套。例如可以在函数定义的内部再定义函数,在函数内部使用单独的大括号,在函数内部使用mod定义模块,等等。

fn main(){
  fn ff(){
    println!("hello world");
  }
  ff();

  let mut a = 33;
  {
    a += 1;
  }
  println!("{}", a);  // 34
}

虽然任何一种大括号都有自己的作用域,但函数作用域比较特别。函数作用域内,无法访问函数外部的变量,而其他大括号的作用域,可以访问大括号外部的变量

fn main() {
  let x = 32;
  fn f(){
    // 编译错误,不能访问函数外面的变量x和y
    // println!("{}, {}", x, y);  
  }
  let y = 33;
  f();

  let mut a = 33;
  {
    // 可以访问大括号外面的变量a
    a += 1;
  }
  println!("{}", a);
}

在Rust中,能否访问外部变量称为【捕获环境】。比如函数是不能捕获环境的,而大括号可以捕获环境。

对于可捕获环境的大括号作用域,要注意Rust的变量遮盖行为。

分析下面的代码:

fn main(){
  let mut a = 33;
  {
    a += 1;   // 访问并修改的是外部变量a的值

    // 又声明变量a,这会发生变量遮盖现象
    // 从此开始,大括号内访问的变量a都是该变量
    let mut a = 44; 
    a += 2;
    println!("{}", a);  // 输出46
  }    // 大括号内声明的变量a失效
  println!("{}", a);   // 输出34
}

这种行为和其他语言不太一样,因此这种行为需要引起注意。

悬垂引用

在支持指针操作的语言中,一不小心就会因为释放内存而导致指向该数据的指针变成悬垂指针(dangling pointer)。

Rust的编译器保证永远不会出现悬垂引用:引用必须总是有效。即引用必须在数据被销毁之前先失效,而不能销毁数据后仍继续持有该数据的引用。

例如,下面的代码不会通过编译:

fn main(){
  let sf = f();  // f()返回值是一个无效引用
}

fn f() -> &String {
  let s = String::from("hello");
  &s  // 返回s的引用
}   // s跳出作用域,堆中String字符串被释放

该示例报错的原因很明显,函数的返回值&s是一个指向堆中字符串数据的引用(注意,引用是一个实实在在的数据),当函数结束后,s跳出作用域,其保存的字符串数据被销毁,这使得返回值&s变成了一个无效的引用。

这里的悬垂指针非常明显,但很多时候会在非常隐晦的情况下导致悬垂指针,幸好Rust保证了绝不出现悬垂指针的问题。