变量与可变性

Rust 的变量系统比大多数语言多了一个维度:可变性由你显式控制,而不是默认允许修改。

声明与绑定

Rust 用 let 关键字声明变量。“变量绑定”这个名字比”变量赋值”更准确——你是在把一个值绑定到一个名字上。

基本语法

fn main() {
    let x = 5;          // 整数,编译器推断为 i32
    let y = 3.14;       // 浮点,推断为 f64
    let z: u8 = 255;    // 也可以显式标注类型
    let flag = true;    // 布尔值

    println!("x={} y={} z={} flag={}", x, y, z, flag);
}

类型推断:Rust 的编译器非常聪明,大多数情况下能从赋值和使用方式推断出变量类型,你不需要每次都写类型注解。当推断不了时,编译器会直接报错告诉你需要补上。

默认不可变

Rust 的变量默认是不可变的——绑定之后,值就不能再改变:

fn main() {
    let x = 5;
    x = 6; // 错误!不能对不可变变量二次赋值
    println!("{}", x);
}

编译器会给出非常清晰的错误信息,甚至告诉你解决方法:

   Compiling playground v0.0.1 (/playground)
error[E0384]: cannot assign twice to immutable variable `x`
 --> src/main.rs:3:5
  |
2 |     let x = 5;
  |         - first assignment to `x`
3 |     x = 6; // 错误!不能对不可变变量二次赋值
  |     ^^^^^ cannot assign twice to immutable variable
  |

为什么要默认不可变?

这是 Rust 最有意思的设计决策之一,值得认真理解。

问题场景:假设你的程序里有这样一段逻辑——

fn calculate_tax(income: f64) -> f64 {
    let rate = 0.20; // 税率 20%,"感觉"不会变

    // 假设中间有很多逻辑……
    let taxable = income * 0.8;

    //...很多行代码...

    // 某个地方悄悄修改了 rate(比如另一个同事写的)
    // rate = 0.25; // 如果是可变的,这行可能潜伏在几百行之后

    //...很多行代码...

    taxable * rate // 这里用的是哪个 rate?
}

fn main() {
    println!("税额: {:.2}", calculate_tax(100_000.0));
}

在大型项目中,rate 可能在函数的前半段设置,在几百行之后的某处被意外修改,导致最终计算结果出错。追踪这类 bug 非常痛苦——你不得不在整个函数里搜索”谁改了这个值”。

Rust 的解法:变量默认不可变。如果 rate 不需要改变,就不加 mut——编译器保证它不会被任何地方修改,你读代码时可以完全放心地说”这个值从声明到用完都是 0.20”。

fn main() {
    let config_value = 42; // 配置项,不应该被修改

    // 几百行之后,某处意外尝试修改它……
    config_value = 99; // 编译器:不行!

    println!("{}", config_value);
}

不可变性 ≠ 性能损失:不可变变量和可变变量在运行时没有性能差异,不可变只是编译期的约束。mut 是你告诉编译器”我真的需要修改这个值”的明确声明,而不是一个优化开关。

先声明,后初始化

可以先声明变量,稍后再给它赋值——但 Rust 绝不允许使用未初始化的变量:

fn main() {
    let result; // 只声明,不赋值

    {
        let base = 4;
        result = base * base; // 在内层作用域里初始化
    }

    println!("result = {}", result); // 可以使用,因为已经初始化了
}
fn main() {
    let x;
    println!("{}", x); // 错误!使用了未初始化的变量
    x = 1;
}

这和 C 语言不同。C 允许使用未初始化的变量(值是不确定的垃圾数据),这是很多 bug 的来源。Rust 编译器在编译期就禁止这种情况,彻底杜绝了”读垃圾值”的问题。

_ 前缀抑制未使用警告

声明了但没有使用的变量,编译器会发出警告。如果某个变量是有意不使用的(比如调试时临时写的),加上 _ 前缀可以告诉编译器”我知道,不用提醒我”:

fn main() {
    let _intentionally_unused = 42; // 不会警告
    let also_unused = 99;           // 会警告:unused variable

    println!("只用这一个");
    // also_unused 从未被读取
    let _ = also_unused;            // 用 let _ = 显式丢弃也可以
}

可变与常量

mut 声明可变变量

在变量名前加 mut,就可以在绑定后修改它的值:

fn main() {
    let mut count = 0;

    count += 1;
    count += 1;
    count += 1;

    println!("count = {}", count); // 3
}

mut 不只是给编译器看的,也是给读代码的人看的——它明确传达”这个变量的值会变化”。没有 mut 的变量,阅读代码时可以放心地认为它的值从始至终不变。

const 声明常量

const 声明的是真正的常量,有几个与 let 不同的规则:

// 常量通常在函数外声明(全局可见),也可以在函数内
const MAX_SCORE: u32 = 100;
const SECONDS_PER_HOUR: u32 = 60 * 60; // 常量表达式,编译时计算

fn main() {
    println!("满分: {}", MAX_SCORE);
    println!("一小时: {} 秒", SECONDS_PER_HOUR);
}

const 的特点:

  • 必须标注类型:编译器不推断常量类型
  • 命名约定:全大写字母 + 下划线分隔(SCREAMING_SNAKE_CASE
  • 只能是常量表达式:不能是函数调用结果或运行时才知道的值
  • 在任意作用域有效:包括全局,整个程序运行期间都存在

let / let mut / const 对比

letlet mutconst
可变否(永远)
必须标注类型否(推断)否(推断)
作用域块作用域块作用域任意(含全局)
能遮蔽
典型用途局部值需要修改的值程序常量、配置值

const 与不可变 let 的本质区别let 默认不可变,但可以被遮蔽(重新绑定);const 是真正的常量,不能被任何操作改变,编译器会把它内联到每个使用处。

作用域与遮蔽

作用域

变量的作用域由 {} 划定——超出大括号,变量就不再存在:

fn main() {
    let outer = "外层";

    {
        let inner = "内层";
        println!("{} 和 {}", outer, inner); // 内层可以访问外层
    }

    println!("{}", outer);  // 正常
    // println!("{}", inner); // 错误!inner 已离开作用域
}

变量遮蔽

let 重新声明同名变量,会遮蔽(shadow)之前的变量——新变量使旧变量失效,在当前作用域内使用新值:

fn main() {
    let x = 5;
    println!("原始 x = {}", x); // 5

    let x = x + 1; // 遮蔽:新 x = 旧 x + 1
    println!("遮蔽后 x = {}", x); // 6

    {
        let x = x * 2; // 内层作用域遮蔽
        println!("内层 x = {}", x); // 12
    }

    println!("离开内层后 x = {}", x); // 回到 6,内层遮蔽消失
}

遮蔽有一个 mut 做不到的能力:改变变量的类型

fn main() {
    // mut 无法做到这件事(类型不能改变)
    let spaces = "   ";           // &str 类型
    let spaces = spaces.len();    // 遮蔽为 usize 类型

    println!("空格数: {}", spaces);

    // 如果用 mut 尝试改类型,会编译报错:
    // let mut spaces = "   ";
    // spaces = spaces.len(); // 错误!类型不匹配
}

遮蔽 vs mut 的选择:如果需要修改同一个值,用 mut;如果想对一个值做一次性转换后得到一个新的不可变绑定,用遮蔽——遮蔽后的变量默认仍是不可变的。

冻结

当一个可变变量被不可变绑定遮蔽时,在该作用域内它就被”冻结”了,不能再修改。离开该作用域后,可变性恢复:

fn main() {
    let mut value = 100;

    {
        let value = value; // 用不可变绑定遮蔽 value,冻结它
        value = 200;       // 错误!value 在此作用域被冻结
    }

    // 离开内层作用域,冻结解除
    value = 200; // 正常!
    println!("{}", value);
}

冻结的本质:遮蔽创建了一个新的不可变变量(名字相同),在它的作用域内,可变的外层变量被”挡住”了,无从访问。

练习题

不可变变量的错误

fn main() {
    let score = 100;
    score = 90;
    println!("{}", score);
}
加载题目中…

mut 的含义

加载题目中…

const 与 let 的区别

加载题目中…

遮蔽的能力

fn main() {
    let x = "hello";
    let x = x.len();
    println!("{}", x);
}
加载题目中…

变量超出作用域

fn main() {
    let x = 1;
    {
        let y = 2;
        println!("{}", x + y);
    }
    println!("{}", y);
}
加载题目中…

未初始化变量

加载题目中…

编程练习 1

下面的代码想实现一个简单的计数器,但有编译错误,请修复它:

fn main() {
    let count = 0;

    count = count + 1;
    count = count + 1;
    count = count + 1;

    println!("count = {}", count);
}

编程练习 2

用遮蔽把字符串 " Rust " 分三步处理:先去掉首尾空白,再转换为大写,最后输出长度。每一步用同名变量 s 遮蔽,不使用 mut

fn main() {
    let s = "  Rust  ";
    // TODO: 第一步:s = s.trim()(去掉首尾空白)
    // TODO: 第二步:s = s.to_uppercase()(转大写)
    // TODO: 第三步:s = s.len()(取长度)
    println!("{}", s);
}