3、基础语法与变量:变量与不可变性、常量、类型推断、基本数据类型(整数、浮点、布尔、字符)、元组与数组、类型转换与溢出处理
Rust 语言的设计哲学中,"内存安全"与"性能"是两大基石,而这一切的起点,正是对变量与数据类型的严谨管理。与 Python、JavaScript 等动态语言不同,Rust 在编译期就强制要求开发者明确数据的类型与可变性,从而在代码运行之前就消除了一整类与内存管理相关的 Bug。本章将深入剖析 Rust 的变量系统,从不可变性的核心思想出发,逐步延伸到常量、类型推断、基本数据类型、复合数据结构(元组与数组),以及类型转换与溢出处理。理解这些基础语法,是构建健壮、高效 Rust 应用的必经之路。
上图清晰地展示了 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
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 的数组越界是运行时错误,不是编译错误。务必检查索引范围。