作用域
作用域(Scope)是 Rust 所有权系统的核心基础之一,理解作用域是理解所有权、借用和生命周期的前提。
作用域概念
作用域是变量在程序中有效的范围。在 Rust 中,任何一对包含代码的花括号都构成一个独立的作用域。变量从绑定处开始有效,到离开所在作用域时失效。
用于定义数据类型的花括号(如
struct { }和enum { })不构成作用域,本章所说的花括号均不包含这种情况。
rust
{ // s 在这里无效,尚未绑定
let s = "hello"; // 从此处起,s 是有效的
println!("{}", s); // 使用 s
} // 作用域结束,s 失效拥有独立作用域的结构
包括但不限于以下几种结构中的花括号都拥有自己的作用域:
| 结构 | 示例 |
|---|---|
| 函数定义 | fn foo() { ... } |
if 语句 | if cond { ... } |
while / loop | while 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):
- 普通代码块(
if、while、单独花括号等): 可以捕获环境 - 函数(
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。