Rust 学习笔记(3/21):通用编程概念——把地基打牢

本系列基于官方《Rust 程序设计语言》(The Rust Programming Language,TRPL)逐章学习。

开篇:盖楼先打地基

前两章我们”跑”通了猜数字游戏,但对很多语法只有模糊印象。第 3 章是全书的地基工程:变量、数据类型、函数、注释、控制流——这些概念几乎所有语言都有,但 Rust 在每个点上都有自己的”脾气”。地基打得牢不牢,直接决定第 4 章所有权这座”大厦”能不能盖起来。

变量与可变性:Rust 的第一个”脾气”

变量默认不可变。试着给 let x = 5; 再赋值,编译器立刻亮红牌:

1
2
error[E0384]: cannot assign twice to immutable variable `x`
help: consider making this binding mutable: `mut x`

这不是找茬:如果代码一部分假设某值永不变、另一部分偷偷改了它,bug 极难追踪。编译器帮你保证”说好不变就不变”。需要可变时加 mut

1
2
let mut x = 5;   // 可变
x = 6;

与之容易混淆的三样东西,用一张表分清:

形式 关键字 可变性 特点
变量(默认) let 不可变 可加 mut 变可变
常量 const 永不可变 必须标注类型,可声明在任意作用域(含全局),值必须是编译期可计算的表达式(如 60 * 60 * 3),命名全大写+下划线
遮蔽 let 复用同名 新绑定 用新值”盖住”旧值,允许改变类型let spaces = " "; let spaces = spaces.len();),转换完成后变量再次不可变

遮蔽与 mut 的核心区别:mut 改变值时不能改变类型let mut spaces = " "; spaces = spaces.len(); 会报 E0308 类型不匹配),而遮蔽本质是创建新变量,所以类型随便换。

数据类型:静态类型的约束与便利

Rust 是静态类型语言,编译期必须知道所有变量类型。编译器通常能靠值和用法推断,但 parse 这类”多义”场景必须显式标注(let guess: u32 = "42".parse()...,否则报 E0282)。

标量类型(单个值)有四种:

类型 说明 要点
整型 i8~i128u8~u128isize/usize 默认 i32isize/usize 随系统架构定,常用于集合索引;字面量可用 _ 分隔(1_000)、前缀(0xff
浮点 f32f64 默认 f64(现代 CPU 上几乎与 f32 同速但精度更高);整数除法向下取整(2 / 3 得 0)
布尔 bool 1 字节,true/false
字符 char 4 字节,Unicode 标量值——emoji、中文都算,用单引号

整型溢出:debug 模式直接 panic;release 模式做二进制补码”包裹”(u8 的 256 变 0)。要显式处理可用 wrapping_*/checked_*/overflowing_*/saturating_* 系列方法。

复合类型(多个值)有两种:

  • 元组(tuple):长度固定、元素类型可以不同。let tup: (i32, f64, u8) = (500, 6.4, 1); 可用模式匹配解构 let (x, y, z) = tup; 或用索引 tup.0 访问;() 是特殊”单元类型”,不返回值时隐式返回它;
  • 数组(array):长度固定、元素类型必须相同。let a: [i32; 5] = [1, 2, 3, 4, 5]; 简写 let a = [3; 5];(5 个 3)。越界访问会在运行时 panic——这是 Rust 安全原则的第一个体现:宁可崩溃退出,也不访问无效内存。需要可增删的集合用 vector(第 8 章)。

函数:参数、语句与表达式

函数名用 snake_case(全小写+下划线),定义位置无关紧要(在 main 之前之后都行)。签名里必须标注每个参数类型(fn another_function(x: i32))。

Rust 是基于表达式的语言,这是理解函数的关键:

  • 语句(statement):执行操作但不返回值,如 let y = 6;
  • 表达式(expression):计算并产生值,如 5 + 6、函数调用、代码块 {}表达式末尾没有分号,加分号就变成语句、不再返回值。

因此函数返回值的写法很特别:函数体最后一个表达式就是返回值,类型写在 -> 后:

1
2
3
4
5
6
7
fn five() -> i32 {
    5        // 没有分号!这是返回值
}

fn plus_one(x: i32) -> i32 {
    x + 1    // 若写成 x + 1; 则报 E0308:expected i32, found ()
}

代码块也是表达式:let y = { let x = 3; x + 1 };y 得到 4。

控制流:if 与三种循环

if 的条件必须是 bool——Rust 不会像 Ruby/JavaScript 那样做真假值隐式转换,if number(number 是整数)会直接报错,必须写 if number != 0else if 会按顺序找第一个为真的分支执行。因为 if 是表达式,可以直接用于赋值:

1
let number = if condition { 5 } else { 6 };   // 两分支类型必须一致,否则报 E0308

三种循环的取舍:

循环 适用场景 关键点
loop 无条件重复,直到显式停止 break 跳出(可带返回值 break counter * 2);continue 跳过本轮;嵌套时可用循环标签 'label: loop { ... break 'label; } 跳出外层
while 条件循环 while number != 0 { ... },比手写 loop+if+break 简洁
for 遍历集合(Rust 使用最多) for element in a 无需管理索引、天然防越界;配合 Range 做定次循环:for number in (1..4).rev()

while 加索引遍历数组易错(数组改长度忘了改条件就 panic),for 则完全没有这个烦恼——这正是它成为 Rust 首选循环的原因。

实践建议与总结

  1. 把”默认不可变”当默认姿势:先写不可变,编译器喊痛再加 mut,这是 Rust 帮你防 bug 的第一道防线;
  2. 善用类型错误信息:本章出现的 E0384/E0308/E0282 都是”老师”,错误信息会直接给出修复建议(如”考虑加 mut”“删除分号”);
  3. 优先 for 而非 while+索引:遍历集合永远选 for,简洁、安全、更符合 Rust 惯例。

本章的收获可以浓缩成一张”地基清单”:变量(默认不可变/常量/遮蔽)、类型(四种标量+两种复合)、函数(参数必标注/表达式即返回值)、注释(//)、控制流(if 必 bool/三种循环选 for)。地基打好,下一章就来啃全书最硬的核心——所有权(ownership),那是 Rust 与其他语言最不同的地方。