变量与不可变性
Rust 的默认:不可变(immutable)
在 Rust 中,用 let 声明的变量默认是不可变的。这与大多数语言相反——在 JavaScript 中你需要用 const 声明常量,而在 Rust 中你需要用 mut 才能声明可变变量。
这个设计决策不是意外,而是深思熟虑的安全哲学:不可变数据更容易推理,不会被意外修改,在并发场景中天然线程安全。如果你尝试修改不可变变量,Rust 编译器会直接报错,而不是让程序在运行时出现难以追踪的 bug。
fn main() {
let x = 5;
println!("x 的值是: {}", x);
// 编译错误!x 是不可变的
// x = 6; // error[E0384]: cannot assign twice to immutable variable `x`
// 声明可变变量,需要显式加 mut
let mut y = 5;
println!("y 的初始值: {}", y);
y = 6; // 合法:y 是可变的
println!("y 的新值: {}", y);
}
在 Rust 代码中,mut 是一个"警示标记"——当你在代码中看到 mut,你立刻知道这个变量可能被修改,需要额外关注。这让代码的意图更加明确,也让代码审查更容易。
变量 vs 常量(const)
Rust 中的 const 和 let 不可变变量有重要区别:
// 常量:必须标注类型,值在编译期确定
const MAX_POINTS: u32 = 100_000; // 下划线可用作数字分隔符
const PI: f64 = 3.14159265358979;
// 静态变量:有固定内存地址
static HELLO_WORLD: &str = "Hello, World!";
fn main() {
println!("最大积分: {}", MAX_POINTS);
println!("π ≈ {}", PI);
}
Shadowing(变量遮蔽)
Rust 允许在同一作用域内用 let 重新声明同名变量,这叫做 shadowing(遮蔽)。新变量会"遮蔽"旧变量,旧变量在遮蔽后不可访问。这与 mut 的可变性完全不同:
fn main() {
let x = 5;
println!("第一个 x: {}", x); // 5
// shadowing:重新声明同名变量(注意:这不是修改!)
let x = x + 1;
println!("第二个 x: {}", x); // 6
{
let x = x * 2;
println!("内部作用域 x: {}", x); // 12
}
println!("外部作用域 x: {}", x); // 6(内部遮蔽不影响外部)
// shadowing 最强大的用法:改变类型!
let spaces = " "; // &str 类型
let spaces = spaces.len(); // usize 类型——同名,但类型改变了!
println!("空格数: {}", spaces); // 3
// 如果用 mut,就无法改变类型
// let mut spaces = " ";
// spaces = spaces.len(); // 编译错误!类型不匹配
}
标量类型(Scalar Types)
整数类型
Rust 的整数类型精确控制了位宽和符号性,让你在设计数据结构时能明确表达意图:
| 类型 | 位宽 | 范围 | 典型用途 |
|---|---|---|---|
i8 | 8位 | -128 ~ 127 | 小整数、字节 |
u8 | 8位 | 0 ~ 255 | 字节数据、颜色分量 |
i16 | 16位 | -32768 ~ 32767 | 音频样本 |
u16 | 16位 | 0 ~ 65535 | 端口号 |
i32 | 32位 | 约 ±21亿 | 默认整数类型 |
u32 | 32位 | 0 ~ 约42亿 | 哈希值、颜色 |
i64 | 64位 | 约 ±9.2×10¹⁸ | 时间戳(纳秒) |
u64 | 64位 | 0 ~ 约1.8×10¹⁹ | 文件大小 |
i128 | 128位 | 极大范围 | 密码学、UUID |
isize | 平台相关 | 指针大小 | 索引、偏移量 |
usize | 平台相关 | 0 ~ 指针最大值 | 集合索引(必须) |
fn main() {
// 各种整数字面量写法
let decimal = 98_222; // 十进制(下划线作分隔符)
let hex = 0xff; // 十六进制
let octal = 0o77; // 八进制
let binary = 0b1111_0000; // 二进制
let byte: u8 = b'A'; // 字节字面量(仅 u8)值为 65
// 类型后缀
let x = 42u8;
let y = -100i64;
println!("{} {} {} {} {}", decimal, hex, octal, binary, byte);
}
在 debug 模式下,整数溢出会导致 panic(程序崩溃)。在 release 模式下,会发生 wrapping(循环)——例如 u8 的 255 + 1 = 0。如果需要明确的溢出行为,应使用 wrapping_add、checked_add、saturating_add 等方法。
浮点类型
fn main() {
let x = 2.0; // f64(默认,IEEE 754 双精度)
let y: f32 = 3.0; // f32(单精度,约7位有效数字)
// 浮点运算
let sum = x + 5.0;
let product = 3.0 * 4.0;
let quotient = 56.7 / 32.2;
let remainder = 43.0 % 5.0;
// 浮点数的特殊值
let inf = f64::INFINITY;
let nan = f64::NAN;
println!("NaN == NaN: {}", nan == nan); // false!NaN 不等于任何值
println!("is_nan: {}", nan.is_nan()); // true
}
布尔类型与字符类型
fn main() {
// 布尔类型:只有 true 和 false
let t = true;
let f: bool = false;
// char 类型:Unicode 标量值,占 4 字节
// 注意:用单引号!字符串用双引号
let c = 'z';
let emoji = '😀'; // 支持任意 Unicode 字符!
let cjk = '中'; // 中文字符
// char 占 4 字节(不是 1 字节!)
println!("char 大小: {} 字节", std::mem::size_of::<char>()); // 4
println!("字符: {} {} {}", c, emoji, cjk);
}
复合类型
元组(Tuple)
元组是将多个不同类型的值组合在一起的固定长度序列。一旦声明,元组的长度不能改变。
fn main() {
// 声明元组
let tup: (i32, f64, u8) = (500, 6.4, 1);
// 解构(destructuring):一次绑定多个值
let (a, b, c) = tup;
println!("a={}, b={}, c={}", a, b, c);
// 用 .index 访问元素(索引从 0 开始)
println!("第一个元素: {}", tup.0);
println!("第二个元素: {}", tup.1);
// 空元组 ():在 Rust 中叫做"unit",是所有函数的默认返回值
let unit: () = ();
println!("unit 大小: {} 字节", std::mem::size_of_val(&unit)); // 0
}
数组(Array)
数组是同类型元素的固定长度序列,分配在栈上(不是堆)。与 Vec(动态数组)不同,数组大小在编译期确定,是 Rust 类型系统的一部分。
fn main() {
// 类型注解: [元素类型; 长度]
let a: [i32; 5] = [1, 2, 3, 4, 5];
// 快捷初始化:所有元素相同
let b = [3; 5]; // 等价于 [3, 3, 3, 3, 3]
// 访问元素
println!("a[0] = {}, a[4] = {}", a[0], a[4]);
// 数组长度
println!("数组长度: {}", a.len()); // 5
// 迭代数组
for element in a {
print!("{} ", element);
}
}
Rust 在运行时检查数组边界。如果你访问 a[10](但数组只有 5 个元素),程序会 panic 并打印错误信息,而不是像 C 那样悄悄读取越界内存(这是严重的安全漏洞)。Rust 宁愿崩溃,也不愿让程序在错误状态下继续运行。
类型推断与类型转换
类型推断(Type Inference)
Rust 拥有强大的类型推断能力——大多数情况下你不需要显式写出类型,编译器能从上下文推断出来:
fn main() {
// 编译器从值推断类型
let x = 42; // i32(整数默认类型)
let y = 3.14; // f64(浮点数默认类型)
let z = true; // bool
// 从后续使用推断类型
let mut vec = Vec::new(); // 暂时未知类型
vec.push(1u8); // 现在编译器知道是 Vec<u8>
// 需要明确类型时再标注
let large: i64 = 100; // 显式指定 i64,不用默认的 i32
}
类型转换(Type Conversion)
Rust 不允许隐式类型转换(这是 C 中大量 bug 的来源)。所有类型转换必须显式进行:
fn main() {
let x: i32 = 42;
// as 关键字:基本类型转换
let y = x as f64; // i32 -> f64(安全,不会丢失信息)
let z = x as u8; // i32 -> u8(截断!可能丢失信息)
let big: i32 = 300;
println!("{}", big as u8); // 44(300 % 256 = 44)
// 字符转换
let c = 'A';
println!("'A' as u32 = {}", c as u32); // 65
// 更安全的转换:使用 From/Into trait
let s = String::from("hello"); // &str -> String
let n: i64 = i64::from(42i32); // i32 -> i64(保证安全)
// 字符串解析
let num: i32 = "42".parse().unwrap();
println!("{} {} {}", y, z, num);
}
函数基础
函数定义与返回值
Rust 的函数设计有一个优雅的特点:函数体中最后一个表达式自动成为返回值,不需要 return 关键字(除非提前返回)。语句(statement)和表达式(expression)的区别很重要:表达式有值,语句没有值(返回空元组 ())。
// 函数参数必须显式标注类型
fn add(x: i32, y: i32) -> i32 {
x + y // 没有分号!这是表达式,是返回值
}
fn add_with_return(x: i32, y: i32) -> i32 {
return x + y; // 显式 return,注意有分号
}
fn describe(x: i32) -> String {
// if-else 也是表达式
if x > 0 {
String::from("正数")
} else if x == 0 {
String::from("零")
} else {
String::from("负数")
}
}
fn main() {
println!("2 + 3 = {}", add(2, 3));
println!("-5 是 {}", describe(-5));
// 代码块也是表达式
let y = {
let x = 3;
x * x + 1 // 没有分号,是这个块的返回值
};
println!("y = {}", y); // 10
}
在 Rust 中,给最后一行加分号会把表达式变成语句,导致函数返回 () 而非期望的类型。这是初学者最常犯的错误之一。如果你看到类似 "expected `i32`, found `()`" 的错误,首先检查函数最后一行是否多了分号。