跳转到内容

Rust 作用域

作用域(scope)是变量有效的那一段代码区域。这个概念很简单,但弄懂了它,Rust 的很多其他规则(尤其是所有权)一下子就说得通了。

一个变量从声明的那一刻开始存在,直到它所在代码块的右花括号 } 为止:

fn main() {
let x = 5; // x 从这里开始进入作用域
{
let y = 10; // y 从这里开始进入作用域
println!("x = {x}, y = {y}"); // 两个都有效
} // y 在这里离开作用域
println!("x = {x}");
// println!("{y}"); // ❌ 错误:y 已经不在作用域里了
}

输出:

x = 5, y = 10
x = 5
▶ 动手试一试

y 只存在于内层的 { } 代码块里。代码块一结束,y 就没了。之后再用它会得到一个编译错误,而不是运行时才冒出来的意外。

作用域是单向的:内层代码块能看到外层代码块的变量(所以内层代码块能打印 x),但外层代码块看不到内层的变量。

参数以及你在函数里创建的变量,只在这次函数调用期间存在:

fn calculate() -> i32 {
let result = 5 * 2; // result 只存在于 calculate 里面
result
}
fn main() {
let value = calculate();
println!("{value}");
// println!("{result}"); // ❌ 错误:在这里 result 不存在
}

输出:

10
▶ 动手试一试

反过来也一样:函数看不到 main 里的局部变量。如果它需要某个值,就像你在函数那一页看到的那样,把它作为参数传进去。

{ } 代码块不只是限定变量的存活范围,它还能产生一个值:也就是块里最后一个不带分号的表达式。这和让函数返回最后一行的规则是同一个。

当你需要几个辅助变量来算出某个结果,又不想让它们之后一直留着时,这个特性就很方便:

fn main() {
let price = 40;
let total = {
let tax = price / 10; // tax 只活在这个代码块里
let shipping = 5;
price + tax + shipping // 没有分号:这就是代码块的值
};
println!("total = {total}");
// println!("{tax}"); // ❌ 错误:tax 已经离开作用域
}

输出:

total = 49
▶ 动手试一试

有两个细节要留意:

  • 最后一行 price + tax + shipping 没有分号。如果加上分号,代码块给出的就是 ()(什么都没有),而不是一个数字,println! 也就没法打印它了。
  • 整个写法是一条 let 语句,所以在右花括号后面要以 }; 结尾。

代码块一结束,tax 和 shipping 就消失了,只剩下结果 total。像这样把辅助变量放进一个小代码块,能清楚地表明它们只是为了这一次计算。

这和你稍后会学到的所有权直接相关:一个值的所有者离开作用域的那一刻,这个值就会被自动清理掉。

fn main() {
{
let text = String::from("你好");
println!("{text}");
} // text 的作用域到这里结束,Rust 会自动丢弃它
}

输出:

你好
▶ 动手试一试

Rust 不需要垃圾回收器,靠的就是这整套机制:它在编译时就已经准确知道每个作用域在哪里结束。

还记得变量那一页讲的遮蔽(shadowing)吗?在一个作用域里,每次用同一个名字写 let,都会创建一个新的变量。它不会抹掉外层的变量,只是在这个作用域剩下的部分里接替它:

fn main() {
let x = 5;
{
let x = x * 2; // 一个新的 x,只在这个代码块里
println!("内层 x = {x}"); // 10
}
println!("外层 x = {x}"); // 5,不受影响
}

输出:

内层 x = 10
外层 x = 5
▶ 动手试一试

下面这个程序无法编译,因为 area 在它的代码块结束之后还被使用:

fn main() {
let width = 3;
{
let height = 4;
let area = width * height;
}
println!("area = {area}");
}

修改它,让它打印出 area = 12,同时 height 仍然只活在代码块里。提示:让代码块产生面积,再把结果存进一个在代码块外面声明的变量。

查看答案
fn main() {
let width = 3;
let area = {
let height = 4;
width * height
};
println!("area = {area}");
}

输出:

area = 12
▶ 动手试一试

现在 area 活在 main 的作用域里,所以 println! 能看到它。height 仍然只在代码块里用到,代码块结束时它就被丢弃了。

创建一个变量 level,值为 1。打开一个代码块,用 2 遮蔽它;在这个代码块里再打开一个代码块,用 3 遮蔽它。在每个代码块里打印 level,并在每个代码块结束后马上再打印一次。提示:你应该看到数字依次是 1、2、3,然后回到 2 和 1。

查看答案
fn main() {
let level = 1;
println!("第 {level} 层");
{
let level = 2;
println!("第 {level} 层");
{
let level = 3;
println!("第 {level} 层");
}
println!("回到第 {level} 层");
}
println!("回到第 {level} 层");
}

输出:

第 1 层
第 2 层
第 3 层
回到第 2 层
回到第 1 层
▶ 动手试一试

每个内层的 level 只在它的代码块结束之前遮住外层的那个。外层变量从来没有被改动过,所以它们一直都在,等着你回来。

  • 作用域是变量有效的区域(用 { } 标出)。
  • 一到作用域的右花括号 },变量就立刻不复存在。
  • 函数体也是作用域:函数返回时,里面的局部变量就消失了。
  • 代码块是表达式:它的最后一行(不带分号)就是它的值,块里的辅助变量在块结束时消失。
  • 作用域准确地告诉 Rust 什么时候丢弃一个值,所以 Rust 不需要垃圾回收器。

小测验

1. 根据本页内容,Rust 为什么不需要垃圾回收器?

2. 在内层代码块里用 let x = x * 2; 遮蔽一个变量,会发生什么?

3. 执行 let total = { let a = 2; a * 10 }; 之后,total 的值是多少?

得分: 0 / 3