C 程序员理解作用域,通常先想到名字可见性:
void handle(void)
{
int offset = 0;
{
int len = 16;
offset += len;
}
/* len 不可见,offset 仍然可见 */
}
Rust 的块作用域也有这个规则:
fn handle() {
let mut offset = 0;
{
let len = 16;
offset += len;
}
// len 不可见,offset 仍然可见
}
但 Rust 的作用域不只决定名字能不能用。它还会影响资源什么时候释放、绑定能不能被遮蔽、引用能不能继续存在,以及哪些名字能成为模块外的 API。
先抓住四件事:
{ ... }会创建新的块作用域let创建的是绑定,不只是“变量声明”- shadowing 是新绑定遮蔽旧绑定,不是重新赋值
- 作用域结束会触发资源释放
块作用域和 C 很像
C 里的局部变量只在块内可见:
if (ready) {
int code = read_code();
handle_code(code);
}
/* code 不可见 */
Rust 也一样:
if ready {
let code = read_code();
handle_code(code);
}
// code 不可见
这点不用神秘化。函数体、if、while、loop、普通 { ... } 都会形成作用域。
手动加一个块也很常见:
let header_len = {
let fixed = 4;
let options = 8;
fixed + options
};
这个块既限制了 fixed 和 options 的可见范围,也产生了一个值给 header_len。
let 是绑定,不只是声明变量
C 的变量声明会创建一个对象名:
int len = 10;
Rust 更常用“绑定”这个词:
let len = 10;
第一层可以理解成:名字 len 绑定到一个值。
默认绑定不可变:
let len = 10;
len = 11; // 编译错误
需要重新赋值时,用 mut:
let mut len = 10;
len = 11;
这和 C 的默认可变不同。Rust 让“这个名字后面会不会重新指向另一个值”在绑定处就能看出来。
shadowing 是新绑定,不是重新赋值
C 同一个块里不能重复定义同名变量:
int len = 10;
int len = 20; // 错误
Rust 允许 shadowing:
let len = "1024";
let len: usize = len.parse().unwrap();
第二个 len 是新的绑定,它遮蔽了第一个 len。这不是重新赋值。
它适合处理“同一个业务概念,经过一步转换后类型变了”的场景:
let path = "./config.toml";
let path = std::path::Path::new(path);
也可以用来把可变过程收束成不可变结果:
let mut checksum = 0_u32;
for byte in data {
checksum += *byte as u32;
}
let checksum = checksum;
最后一行创建了新的不可变绑定。后面的代码不能再改 checksum。
作用域结束会释放资源
C 里普通局部变量离开作用域会失效,但堆内存和文件句柄通常要手工释放:
FILE *fp = fopen(path, "r");
if (!fp) {
return -1;
}
/* ... */
fclose(fp);
Rust 里很多资源由值自己管理。作用域结束时会自动调用 Drop:
fn read_config(path: &str) -> std::io::Result<String> {
let text = std::fs::read_to_string(path)?;
Ok(text)
}
String、Vec、文件对象、锁守卫等类型离开作用域时,会释放自己拥有的资源。
可以手动缩短作用域:
{
let file = std::fs::File::open(path)?;
// 使用 file
} // file 在这里关闭
// 后面不再持有 file
这不是完整所有权理论,但先建立一个直觉:Rust 的作用域同时是名字边界,也是资源边界。
内层可以借用外层,但不能反过来
C 里返回局部变量地址是典型错误:
int *bad(void)
{
int value = 10;
return &value;
}
Rust 会在编译期拦住这类引用逃出作用域的问题。
第一层先看这个形状:
let value = 10;
let r = &value;
println!("{r}");
r 可以使用,是因为 value 还活着。
但这种形状不行:
let r;
{
let value = 10;
r = &value;
}
println!("{r}"); // 编译错误
value 在内层作用域结束时已经失效,外层不能继续使用指向它的引用。
这一点后面会在生命周期里系统讲。现在只要记住:Rust 的作用域会参与检查“引用不能比被引用的数据活得更久”。
名字可见性不等于 API 可见性
C 里文件作用域的 static 常用来限制符号只在当前 .c 文件内可见:
static int parse_line(const char *line);
Rust 的模块可见性用 pub 表达。默认情况下,模块里的项对外不可见:
fn parse_line(line: &str) -> usize {
line.len()
}
要暴露出去,写 pub:
pub fn parse_line(line: &str) -> usize {
line.len()
}
第一层可以这样理解:
不写 pub -> 当前模块内部使用
写 pub -> 允许从模块外访问
完整的 crate、module、pub(crate)、pub(super)、use 规则更适合放在模块组织里理解。这里先区分两个问题:
- 作用域:一个名字在代码块里能不能被看见
- 可见性:一个模块里的项能不能作为 API 暴露出去
static 是全局存储,不是普通局部变量
C 的文件作用域变量:
static int counter;
它有静态存储期,生命周期覆盖整个程序运行期。
Rust 也有 static:
static APP_NAME: &str = "iot-tool";
static 有固定存储位置,适合少量全局常量或真正需要全局地址的对象。
但不要把 static mut 当成 C 全局变量的直接替代:
static mut COUNTER: u32 = 0;
访问可变全局状态涉及并发和数据竞争,通常需要 unsafe 或同步类型。普通配置常量优先用 const,普通状态优先放进结构体或明确的 owner 里。
最小迁移表
C block scope -> Rust block scope
C local variable -> Rust let binding
C mutable local -> Rust let mut
C same-scope redeclare -> Rust shadowing creates a new binding
C stack variable out of scope -> Rust binding no longer usable
C manual cleanup -> Rust Drop at scope end
C return local address -> Rust rejected by borrow checking
C file-static function -> Rust private item by default
C exported function -> Rust pub fn
C static storage object -> Rust static
Rust 的作用域不是一个孤立语法点。它同时影响名字、资源和 API 边界。C 里很多要靠注释和习惯维护的范围约定,Rust 会尽量变成编译器能检查的边界。