3、基础语法与变量:变量与不可变性、常量、类型推断、基本数据类型(整数、浮点、布尔、字符)、元组与数组、类型转换与溢出处理

Rust 语言的设计哲学中,"内存安全"与"性能"是两大基石,而这一切的起点,正是对变量与数据类型的严谨管理。与 Python、JavaScript 等动态语言不同,Rust 在编译期就强制要求开发者明确数据的类型与可变性,从而在代码运行之前就消除了一整类与内存管理相关的 Bug。本章将深入剖析 Rust 的变量系统,从不可变性的核心思想出发,逐步延伸到常量、类型推断、基本数据类型、复合数据结构(元组与数组),以及类型转换与溢出处理。理解这些基础语法,是构建健壮、高效 Rust 应用的必经之路。

Rust 变量与数据类型核心概念关系图 let 变量绑定 不可变性 (Immutability) 默认不可变,线程安全基石 mut 关键字显式声明可变 常量 (const) 编译期求值,不可变 必须标注类型,全大写命名 类型推断 编译器自动推导类型 减少冗余,保持代码清晰 基本数据类型 整数 (i32, u64...) 浮点 (f32, f64) 布尔 (bool) / 字符 (char) 元组 (tuple) / 数组 (array) 类型转换 (as) 与溢出处理 (debug/release)

上图清晰地展示了 Rust 变量系统的核心框架。一切从 let 绑定开始,它默认赋予变量不可变性,这是 Rust 最独特的特性之一。同时,常量与变量在编译期和运行期有着本质区别。类型推断机制让代码在保持安全的同时不失简洁,而基本数据类型则构成了所有复杂逻辑的原子单元。最后,类型转换与溢出处理是 Rust 在运行时保护程序免受意外错误的重要防线。接下来,我们将逐一拆解这些概念。

3.1 核心原理:变量不可变性与常量

在 Rust 中,使用 let 关键字声明的变量默认是不可变(Immutable)的。这意味着一旦一个值被绑定到一个名称上,你就无法再修改这个值。这一设计并非限制,而是为了安全。在多线程或复杂逻辑中,不可变性从根本上消除了数据竞争(Data Race)的可能性,因为数据一旦创建就永远不会被改变,无需加锁保护。

如果确实需要修改变量,可以使用 mut 关键字显式声明其可变性。这相当于向编译器和其他开发者明确传达:"这个变量是会被修改的",从而让代码的意图更加清晰。

fn main() {
    let x = 5;          // 不可变变量
    // x = 6;           // 编译错误:cannot assign twice to immutable variable
    let mut y = 10;     // 可变变量
    y = 20;             // 允许修改
    println!("x = {}, y = {}", x, y);
}

与变量不同,常量(Constant)使用 const 关键字声明,并且永远不可变。常量必须在声明时显式标注类型,且只能使用常量表达式(编译期可求值的值)进行赋值。常量在编译时会被内联到使用它的地方,因此没有运行时开销。按照 Rust 风格指南,常量名使用全大写字母加下划线分隔。

const MAX_POINTS: u32 = 100_000; // 必须标注类型,且值在编译期确定
经验提醒:优先使用不可变变量。只有在确实需要修改状态时(如循环计数器、累加器),才使用 mut。这能显著降低代码的认知负担,并让编译器帮你捕获意外的状态变更。

3.2 类型推断与基本数据类型

Rust 是静态类型语言,但编译器具备强大的类型推断(Type Inference)能力。当你编写 let guess = 42; 时,编译器会默认推断为 i32(32 位有符号整数)。如果后续代码将其作为浮点数使用,编译器会报错,这保证了类型安全。

Rust 的基本数据类型分为四类:整数、浮点数、布尔和字符。

类型 子类 描述与示例
整数 有符号 (i8, i16, i32, i64, i128, isize) 可表示正负数。默认类型为 i32。isize 取决于系统架构(32/64位)。
无符号 (u8, u16, u32, u64, u128, usize) 仅表示非负数。常用于位运算、索引、集合长度。usize 用于数组索引。
浮点 f32, f64 IEEE 754 标准。默认类型为 f64,在现代 CPU 上性能与 f32 相当,但精度更高。
布尔 bool 仅有两个值:true 和 false。用于条件判断。
字符 char Unicode 标量值,占 4 字节。用单引号表示:'A', '中', '😀'。

3.2.1 元组与数组

元组(Tuple)是一种将多个不同类型的值组合在一起的复合类型。元组的长度固定,一旦声明就无法增加或减少元素。访问元组元素可以使用点号加索引(从 0 开始)。

let tup: (i32, f64, char) = (500, 6.4, 'R');
let (a, b, c) = tup; // 解构
println!("a = {}, b = {}, c = {}", a, b, c);
println!("第一个元素: {}", tup.0);

数组(Array)与元组不同,它要求所有元素必须具有相同的类型。数组的长度也是固定的,存储在栈上。如果需要一个可增长的集合,应使用标准库中的 Vec。

let months = ["January", "February", "March"];
let first = months[0]; // 访问元素
// let invalid = months[10]; // 运行时 panic: index out of bounds
风险说明:数组越界访问在 Rust 中不会像 C 语言那样产生未定义行为,而是会直接触发 panic(程序崩溃)。这是 Rust 内存安全的重要体现——它宁可让程序崩溃,也不允许读取非法内存。

3.3 类型转换与溢出处理

Rust 要求类型转换必须是显式的,这避免了隐式转换可能带来的精度损失或逻辑错误。使用 as 关键字进行强制类型转换。

let a: i32 = 100;
let b: i64 = a as i64;      // 安全:小类型转大类型
let c: u8 = a as u8;        // 可能溢出:如果 a > 255,会截断为低 8 位
let d: f64 = a as f64;      // 整数转浮点

溢出处理是 Rust 的一个关键特性。在调试模式(Debug)下,整数溢出会直接触发 panic。而在发布模式(Release)下,Rust 默认使用二进制补码环绕(Two's Complement Wrapping),即溢出后数值会从最小值重新开始计算。这种差异容易导致开发与生产环境行为不一致,因此需要显式处理。

fn main() {
    let x: u8 = 255;
    // 调试模式:panic
    // let y = x + 1;
    
    // 显式处理溢出
    let y = x.wrapping_add(1);      // 结果为 0
    let z = x.saturating_add(1);    // 结果为 255,饱和到最大值
    println!("wrapping: {}, saturating: {}", y, z);
}

3.4 案例拆解:温度转换器与数据校验

为了综合运用上述知识,我们设计一个华氏度转摄氏度的命令行工具。该案例涉及变量绑定、不可变性、浮点类型、整数与浮点转换,以及溢出检查。

背景与目标:编写一个函数,接收一个华氏温度(整数),返回对应的摄氏温度(浮点数),并处理可能的极端输入。

关键输入与参数:输入为 i32 类型的华氏温度值。转换公式为:C = (F - 32) * 5 / 9。

分析判断过程:由于公式涉及除法,结果应为浮点数。我们需要将整数 F 转换为 f64 再进行计算,以避免整数除法截断。

实际操作步骤:

fn fahrenheit_to_celsius(f: i32) -> f64 {
    // 先将 i32 转换为 f64,再进行浮点运算
    (f as f64 - 32.0) * 5.0 / 9.0
}

fn main() {
    let temp_f: i32 = 98; // 人体正常体温
    let temp_c = fahrenheit_to_celsius(temp_f);
    println!("{}°F = {:.2}°C", temp_f, temp_c);
    
    // 处理极端值:使用 saturating_sub 避免下溢
    let extreme: i32 = i32::MIN;
    let safe_val = extreme.saturating_sub(32); // 不会 panic
    println!("极端值处理: {}", safe_val);
}

结果对比与复盘:输出结果为 98°F = 36.67°C,符合预期。通过显式类型转换,我们确保了浮点运算的精度。对于极端值,saturating_sub 方法避免了在调试模式下因下溢导致的程序崩溃,体现了 Rust 对边界情况的严谨处理。

3.5 常见误区与总结复盘

在学习 Rust 变量与类型时,新手常犯以下错误:

  • 混淆变量与常量:变量用 let,常量用 const。常量必须标注类型,且不能使用 mut。
  • 忽视整数溢出:在调试模式下,溢出会导致 panic。在发布模式下,静默环绕可能产生逻辑错误。应使用 wrapping_*、saturating_* 或 checked_* 方法显式处理。
  • 隐式类型转换的思维惯性:Rust 不允许 let x: i32 = 3.14; 这样的隐式转换,必须使用 as 显式转换。
  • 数组越界:Rust 的数组越界是运行时错误,不是编译错误。务必检查索引范围。
总结复盘:本章的核心在于理解 Rust 如何通过编译期检查来保证运行时安全。不可变性是默认选择,类型推断让代码简洁,显式转换和溢出处理则消除了大量潜在的未定义行为。掌握这些基础,后续学习所有权(Ownership)和生命周期(Lifetime)时,你将更容易理解 Rust 为何能同时提供高性能与高安全性。