Skip to content

作用域

作用域(Scope)是 Rust 所有权系统的核心基础之一,理解作用域是理解所有权、借用和生命周期的前提。

作用域概念

作用域是变量在程序中有效的范围。在 Rust 中,任何一对包含代码的花括号都构成一个独立的作用域。变量从绑定处开始有效,到离开所在作用域时失效。

用于定义数据类型的花括号(如 struct { }enum { })不构成作用域,本章所说的花括号均不包含这种情况。

rust
{                      // s 在这里无效,尚未绑定
    let s = "hello";   // 从此处起,s 是有效的
    println!("{}", s); // 使用 s
}                      // 作用域结束,s 失效

拥有独立作用域的结构

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

结构示例
函数定义fn foo() { ... }
if 语句if cond { ... }
while / loopwhile cond { ... }
match 表达式match x { ... }
mod 模块mod m { ... }
单独的花括号{ ... }

其中,单独的花括号是最简单的作用域形式,可以在任意位置使用:

rust
fn main() {
    let a = 8u32;
    {
        let b = 5u32;
        println!("a = {}", a); // ✅ 内层可以访问外层的 a
        println!("b = {}", b);
    }                          // b 在此失效
    println!("a = {}", a);     // ✅ a 仍然有效
    println!("b = {}", b);     // ❌ b 已经失效
}

变量作用域示意图:

rust
fn main() {
    let a = 8u32;                 // -------------------------|
    {                             //                          |
        let b = 5u32;             // -------|                 |
        println!("a = {}", a);    //        |-- b 的作用域     |--- a 的作用域
        println!("b = {}", b);    //        |                 |
    }                             // -------|                 |
    println!("a = {}", a);        //                          |
}                                 // -------------------------|

作用域结束时的资源回收

当变量离开作用域时,Rust 会自动调用该类型的 drop函数来回收它所占用的内存资源,无需手动管理内存。

rust
{
    let s = String::from("hello"); // s 绑定了一个堆上的字符串
    println!("{}", s);
}  // s 离开作用域,自动调用 drop,堆上的字符串被释放

关于字符串字面量的细节: 字符串字面量(如 let a : &str = "hello")存储在程序的全局只读内存中,从程序启动到终止都一直存在,不会被 drop 销毁。因此,字面量变量离开作用域时,只是变量名失效了,字符串数据本身并不会被释放。一般情况下不需要关注这个细节,我们统一描述为"变量离开作用域时,其绑定的值被销毁"。

作用域嵌套

作用域可以任意嵌套,子作用域可以访问父作用域中的变量,反之则不行。

rust
fn main() {
    fn inner() {
        println!("inner function");
    }
    inner();               // ✅ 在定义所在的作用域内可以调用

    let mut a = 33;
    {
        a += 1;            // ✅ 访问并修改外部变量 a
        println!("{}", a); // 34
    }
    println!("{}", a);     // 34
}

函数作用域

虽然所有花括号都有自己的作用域,但函数作用域有一个重要的特殊规则:

  • 函数内部无法直接访问函数外部的变量,这与某些动态类型的语言完全不同。
rust
fn main() {
    let x = 32;

    fn f() {
        println!("{}", x); // ❌ 编译错误: 函数不能访问外部变量
    }

    let mut a = 33;
    {
        a += 1;            // ✅ 普通代码块可以访问外部变量
    }
    println!("{}", a);     // 34
}

在 Rust 中,能访问外部变量的能力称为捕获环境(Capture Environment):

  • 普通代码块(ifwhile、单独花括号等): 可以捕获环境
  • 函数(fn 定义的函数): 不可以捕获环境
  • 闭包(|| { ... }): 可以捕获环境,这是闭包与函数的重要区别之一,详见闭包

作用域与变量遮蔽

在可以捕获环境的作用域(代码块)中,要特别注意变量遮蔽(Shadowing)的行为。

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

        let mut a = 44;    // 绑定了同名变量,发生变量遮蔽
                           // 从此开始,块内访问的 a 都是这个新变量
        a += 2;
        println!("{}", a); // 输出 46
    }                      // 块内绑定的 a 在此失效
    println!("{}", a);     // 输出 34,外部的 a 不受影响
}

这里有两个容易混淆的概念:

  • 遮蔽前的 a += 1 修改的是外部的 a(可变变量的覆盖)
  • let mut a = 44 创建了一个全新的变量,遮蔽了外部的 a(变量遮蔽)

关于遮蔽(Shadowing)和覆盖(Overwriting)的详细区别,参见变量和可变性章节。

非词法作用域生命周期

  • Non-Lexical Lifetimes,简称 NLL
  • 如果一个变量在之后的地方再也没被用到,那么它的借用就在它最后一次被使用的地方结束。
rust
fn main() {
    let mut s = String::from("hello");
    let r1 = &mut s;
    let r2 = &mut *r1;

    *r2 = String::from("world");    // 通过 r2 修改,没问题
    dbg!(r2);                       // r2 的生命周期到这里结束

    *r1 = String::from("world123"); // 现在 r2 不再被使用,r1 恢复了控制权
    dbg!(r1);
}

在旧版本的 Rust 中,变量的生命周期必须持续到大括号 } 结束。但在现在的 Rust 中,编译器会进行控制流分析:

  • 在这段代码里,dbg!(r2); 是程序中最后一次出现 r2 的地方。
  • 编译器扫描后面的代码发现,第 9 行和第 10 行只用到了 r1,完全没有 r2 的影子。
  • 于是,编译器"聪明"地判断:r2 的使命已经完成了,它占用的对 *r1 的可变借用权限可以立即释放,归还给 r1

基于 MIT 协议发布