Chapter 02

基础语法:变量与类型系统

探索 Rust 的变量设计哲学——为何"默认不可变"是一种安全智慧

变量与不可变性

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);
}
mut 的设计哲学

在 Rust 代码中,mut 是一个"警示标记"——当你在代码中看到 mut,你立刻知道这个变量可能被修改,需要额外关注。这让代码的意图更加明确,也让代码审查更容易。

变量 vs 常量(const)

Rust 中的 constlet 不可变变量有重要区别:

let(不可变变量)
值在运行时确定,可以通过 shadowing 重新绑定,可以在任何作用域内声明。
const(常量)
值必须在编译期确定(编译期常量表达式),必须显式标注类型,可以在全局作用域声明,命名约定为全大写下划线分隔。
static(静态变量)
程序整个生命周期内存在,有固定内存地址(可以取到它的引用),可以是可变的(但需要 unsafe)。
// 常量:必须标注类型,值在编译期确定
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 的整数类型精确控制了位宽和符号性,让你在设计数据结构时能明确表达意图:

类型位宽范围典型用途
i88位-128 ~ 127小整数、字节
u88位0 ~ 255字节数据、颜色分量
i1616位-32768 ~ 32767音频样本
u1616位0 ~ 65535端口号
i3232位约 ±21亿默认整数类型
u3232位0 ~ 约42亿哈希值、颜色
i6464位约 ±9.2×10¹⁸时间戳(纳秒)
u6464位0 ~ 约1.8×10¹⁹文件大小
i128128位极大范围密码学、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_addchecked_addsaturating_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 `()`" 的错误,首先检查函数最后一行是否多了分号。