Rust 变量与可变性

这一篇讲 Rust 的变量声明。Rust 在变量上有一个非常独特的设计——默认不可变。这听起来像是把人绑住,实际上它是 Rust 安全性的重要基石,逼着你写更可控的代码。理解了可变性,你才能理解后面所有权和借用为何这样设计。

1. let 声明变量

Rust 用 let 关键字声明变量。和很多语言不同,Rust 的变量默认是不可变的(immutable)——一旦赋值就不能再改。看例子:

fn main() {
    let x = 5;          // 默认不可变
    println!("x = {}", x);

    // x = 6;            // 编译错误!不能给不可变变量赋值

    // 要修改,必须显式加 mut 关键字
    let mut y = 10;
    println!("y = {}", y);
    y = 20;             // OK,因为声明了 mut
    println!("y = {}", y);
}

第一次接触会觉得反直觉:"变量怎么不能变?"。但这正是 Rust 的刻意设计。

2. 为什么默认不可变?

考虑这样一个场景:你有一个配置对象,在函数开头读进来,然后传给十几个函数处理。如果变量可变,你无法保证中间某个函数没偷偷改它——这种 bug 排查起来非常痛苦(典型的"幽灵数据"问题)。Rust 让变量默认锁死,只有显式声明 mut 才能改:

// 默认不可变的好处:防止"不小心改了不该改的值"
fn main() {
    let config = load_config();
    // config 在整个函数里都不会被改——可以放心
    process(config);          // 拿到的一定是最初的值
    save(config);             // 不会被中间某个函数偷偷改掉
}

// 如果某个变量确实需要变,加 mut 是"明确告诉读者:这里会变"
// 代码审查时,mut 像一个显眼的标记

好处有两个:

如果你之前用 const 风格的 JS/TS(用 const 多过 let),会发现 Rust 的理念是一致的——只是更彻底,把 const 设成了默认。

3. Shadowing(遮盖):用同名变量覆盖

Rust 有一个独特的特性叫 shadowing。你可以用 let 重新声明一个同名变量,新变量会"遮盖"旧的。这和 mut 完全不同:

fn main() {
    let x = 5;
    let x = x + 1;        // shadowing:用同名变量遮盖旧的
    println!("x = {}", x);  // x = 6

    {
        let x = x * 2;    // 内层作用域再 shadow 一次
        println!("内层 x = {}", x);  // 12
    }
    println!("外层 x = {}", x);  // 6,内层的 shadow 不影响外层

    // shadowing 最强大的地方:可以改类型!
    let spaces = "   ";    // &str 类型
    let spaces = spaces.len();  // usize 类型,合法

    // 对比:用 mut 就不能这样,因为类型不能变
    // let mut count = 5;
    // count = "六";        // 编译错误,类型不匹配
}

shadowing 最强大的地方在于可以改变类型。比如先有一个字符串表示空格数,然后想转成数字——你可以用同名变量,而不是起一个 spaces_strspaces_count 这种啰嗦的名字。这在"对同一份数据做逐步转换"的场景下非常优雅,是 Rust 风格的代码标志之一。

注意 shadowing 和 mut 的区别:

4. 常量 const

常量用 const 声明,和不可变变量相似但有几个关键区别:

// 常量:用 const 声明,必须标注类型,值必须编译期可计算
const MAX_USERS: u32 = 100_000;     // 数字可以用下划线分隔,易读
const PI: f64 = 3.14159265;
const GREETING: &str = "你好";

// 常量命名约定:全大写 + 下划线
// 常量可以在任何作用域声明,包括全局
// 常量不能 shadow,不能加 mut

fn main() {
    println!("最多 {} 个用户", MAX_USERS);
    println!("圆周率是 {}", PI);
}

// static:静态变量(有固定内存地址,生命周期等于整个程序)
static COUNTER: u32 = 0;       // 不可变 static
// 可变 static 在多线程下不安全,必须包在 unsafe 里访问
static mut STATE: i32 = 0;     // 极少使用,通常用原子类型替代

const 与 let 的区别:

简单记:全局不变的值用 const;运行时算出来但不再变的值用 let;需要在多处共享且固定地址的用 static(罕见)。

5. 变量作用域

Rust 的作用域规则和大多数 C 系语言一致:花括号定义作用域,变量在离开作用域时失效。这对后面理解所有权至关重要——Rust 的内存释放就是基于作用域的:

fn main() {
    let a = 1;            // s 在外层作用域有效

    {
        let b = 2;        // b 只在内层作用域有效
        let a = 3;        // shadow 外层的 a,只在内层有效
        println!("内层 a = {}, b = {}", a, b);  // 3, 2
    }                    // b 离开作用域,被释放;内层 a 也失效

    println!("外层 a = {}", a);  // 1,恢复成外层那个 a
    // println!("{}", b); // 编译错误!b 已不存在
}

当变量离开作用域,Rust 会自动调用它的"析构"(drop),释放资源。这不需要你手动写 free/close,是所有权系统的核心机制之一。下一篇讲数据类型时我们会更具体看到这一点。

6. 类型推断与显式标注

Rust 是静态类型语言,但绝大多数时候不需要写类型——编译器会从上下文推断。只有在推断不出(或想让代码更清晰)时才需要标注:

fn main() {
    // 类型推断:不写类型,Rust 自动推断
    let x = 5;             // 推断为 i32(默认整数类型)
    let y = 2.5;           // 推断为 f64(默认浮点)
    let z = true;          // bool
    let c = 'A';           // char

    // 显式标注类型(类型标注用冒号)
    let big: i64 = 9_000_000_000;
    let ratio: f32 = 0.5;
    let name: &str = "小明";

    // 让编译器推断一个难以推断的类型时,标注很有用
    let guess: u32 = "42".parse().expect("不是数字");
    println!("{}", guess);  // 42
}

数字字面量默认推断为 i32(整数)和 f64(浮点),这是为了减少标注。如果实际需要别的位宽,加冒号标注即可。数字可以用下划线分隔(如 1_000_000),增强可读性,不影响值。

7. 没有声明的变量不能用

Rust 强制"先声明再使用",没有 hoisting(JS 的变量提升)这类机制。在使用一个变量前必须先 let 它,且必须初始化——未初始化的变量不能读取。这避免了"读到垃圾值"的经典 bug:

小结

这一篇你掌握了 Rust 变量的核心:默认不可变(用 mut 才能改)、shadowing(同名新变量,类型可变)、const 常量作用域决定生命周期。最该记住的理念是:Rust 把"不可变"作为默认值,把 mut 当作一个显眼的标记。下一篇我们看数据类型——整数、浮点、元组、数组,Rust 的类型系统有几个独特之处。

← 上一篇 Rust 基础语法

下一篇 Rust 数据类型

✈️💬