Rust《程序设计语言》 第3章:常见编程概念
Rust《程序设计语言》 第3章:常见编程概念
1. 本章定位
第 3 章介绍的是几乎所有编程语言都会出现的基础概念,但重点在于说明这些概念在 Rust 中的表现形式、语义约束和使用惯例。其核心目标不是追求“会写”,而是建立一套符合 Rust 风格的基础认知框架,为后续学习所有权、结构体、枚举、错误处理与泛型打下语义基础。
本章主要覆盖五个主题:
- 变量与可变性
- 数据类型
- 函数
- 注释
- 控制流
此外,本章开头还特别提醒:Rust 有一组保留关键字,不能作为变量名或函数名使用;有些关键字已经有明确用途,有些则保留给未来语言扩展。
2. 变量与可变性
2.1 Rust 中变量默认不可变
Rust 中变量默认是不可变的,也就是说,一旦某个值绑定给变量名,默认不能再修改它。
let x = 5;
x = 6; // 编译错误
这不是语法上的“刻意刁难”,而是 Rust 用来强化程序可推理性、安全性和并发友好性的设计。其背后的含义是:
- 如果某段代码声明一个值不会变,编译器就保证它真的不会变。
- 这样可以减少“某处偷偷修改数据”导致的隐蔽 bug。
- 阅读代码时,开发者不必额外追踪变量是否在别处被改写。
因此,Rust 倾向于把“可变性”视为一种需要显式声明的能力,而不是默认行为。
2.2 使用 mut 使变量可变
如果确实需要修改变量值,就必须在绑定时显式加上 mut:
let mut x = 5;
x = 6;
这里表达的是:
x绑定到一个值;- 该绑定允许后续重新赋新值;
- 可变性是程序员主动声明的意图,而不是默认存在。
这使代码的“修改风险点”更容易被识别。
2.3 常量 const
常量和不可变变量相似,但两者并不相同。
常量有以下关键特征:
- 用
const声明,而不是let。 - 必须显式标注类型。
- 永远不可变,不能加
mut。 - 只能绑定到常量表达式,不能绑定运行时才能确定的结果。
- 可以定义在任意作用域,包括全局作用域。
示例:
const THREE_HOURS_IN_SECONDS: u32 = 60 * 60 * 3;
2.4 常量的命名约定
Rust 约定常量名全部使用大写字母,并用下划线分隔单词:
MAX_POINTSTHREE_HOURS_IN_SECONDS
这种命名方式有两个作用:
- 强化“这是全局不变语义”的信号;
- 提高阅读时的识别效率。
2.5 常量与不可变变量的区别
| 对比项 | 不可变变量 | 常量 |
|---|---|---|
| 声明关键字 | let | const |
| 是否必须写类型 | 通常可省略 | 必须显式标注 |
| 是否可在运行时求值 | 可以 | 不可以,只能是常量表达式 |
| 生命周期/有效性 | 受作用域限制 | 可定义为全局,且在其作用域内始终有效 |
能否用 mut | 不能 | 不能 |
2.6 遮蔽(shadowing)
Rust 允许使用同一个变量名重新绑定一个新值,这叫做遮蔽。
let x = 5;
let x = x + 1;
{
let x = x * 2;
println!("{x}"); // 12
}
println!("{x}"); // 6
遮蔽的本质不是“修改原变量”,而是:
- 使用同名重新创建一个新变量;
- 后续对该名字的访问会指向新的绑定;
- 旧绑定仍然存在于原先作用域内,但被新绑定遮住了。
2.7 遮蔽与 mut 的区别
这是本节最容易混淆的点之一。
mut 的含义
- 同一个变量绑定被重复赋值。
- 变量类型不能改变。
遮蔽的含义
- 创建了一个新的同名变量。
- 可以改变值,也可以改变类型。
例如:
let spaces = " ";
let spaces = spaces.len();
这里第一次 spaces 是字符串切片类型,第二次 spaces 是数字类型。若使用 mut 则做不到这一点:
let mut spaces = " ";
spaces = spaces.len(); // 编译错误,类型不匹配
2.8 本节要点
- Rust 默认不可变,强调可推理性与安全性。
- 需要修改时必须显式使用
mut。 const用于真正意义上的常量值,且必须写类型。- 遮蔽不是修改,而是重新绑定;它可以改变类型,是值转换时很常见的手法。
3. 数据类型
3.1 Rust 是静态类型语言
Rust 在编译阶段必须知道所有变量的类型。很多时候编译器可以根据赋值和使用方式自动推断类型,但一旦存在多种可能类型,程序员就必须通过类型注解告诉编译器期望的类型。
例如:
let guess: u32 = "42".parse().expect("Not a number!");
若不写 : u32,编译器无法知道 parse() 应该解析成哪种数值类型。
3.2 两大类数据类型
Rust 把数据类型分成两大类:
- 标量类型(scalar):一个值
- 复合类型(compound):多个值组合成一个整体
3.3 标量类型
Rust 有四类基础标量类型:
- 整型
- 浮点型
- 布尔型
- 字符型
3.3.1 整型
整型是没有小数部分的数字。
Rust 内建整型如下:
| 长度 | 有符号 | 无符号 |
|---|---|---|
| 8-bit | i8 | u8 |
| 16-bit | i16 | u16 |
| 32-bit | i32 | u32 |
| 64-bit | i64 | u64 |
| 128-bit | i128 | u128 |
| 与架构相关 | isize | usize |
有符号与无符号
- 有符号:可表示负数,例如
i32 - 无符号:只能表示非负数,例如
u32
默认整型
当没有额外上下文时,Rust 默认整数类型是 i32。
isize 与 usize
这两者取决于机器架构:
- 64 位平台上通常是 64 位
- 32 位平台上通常是 32 位
它们经常用于索引集合。
整数字面值写法
Rust 支持多种整数字面值表示方式:
| 写法 | 示例 |
|---|---|
| 十进制 | 98_222 |
| 十六进制 | 0xff |
| 八进制 | 0o77 |
| 二进制 | 0b1111_0000 |
字节字面值(仅 u8) | b'A' |
其中下划线 _ 仅用于增强可读性,不影响数值本身。
3.3.2 整型溢出
如果一个整数超出类型表示范围,就发生整型溢出。
例如:
u8的取值范围是0..=255- 若试图赋值
256,就会溢出
Rust 在不同构建模式下处理方式不同:
Debug 模式
- 编译器插入溢出检查
- 一旦溢出,程序在运行时
panic!
Release 模式
- 不做导致 panic 的溢出检查
- 采用二进制补码回绕(wrapping)行为
- 例如
u8的256会变成0
这种差异说明:
- Debug 模式更适合发现 bug
- Release 模式更关注性能
- 程序不应依赖隐式回绕行为,除非明确就是要这种语义
显式处理溢出的方式
Rust 标准库提供了几类方法:
wrapping_*:发生回绕checked_*:溢出返回Noneoverflowing_*:返回结果和是否溢出的布尔值saturating_*:溢出时钳制到最大值或最小值
3.3.3 浮点型
Rust 有两种原生浮点类型:
f32f64
默认类型是 f64,因为现代 CPU 上它通常与 f32 速度相近,但精度更高。
let x = 2.0; // f64
let y: f32 = 3.0; // f32
浮点数遵循 IEEE-754 标准。
3.3.4 数值运算
Rust 支持常见数值运算:
+加法-减法*乘法/除法%取余
需要注意:
- 整数除法会向 0 截断
- 例如
-5 / 3 == -1
3.3.5 布尔类型
布尔类型是:
truefalse
Rust 中布尔类型写作 bool:
let t = true;
let f: bool = false;
布尔值主要用于条件判断,例如 if 表达式。
3.3.6 字符类型 char
Rust 的 char 是语言中最原始的字符类型,用单引号表示:
let c = 'z';
let z: char = 'ℤ';
let heart_eyed_cat = '😻';
关键点:
char占 4 字节- 表示的是一个 Unicode 标量值,而不是单纯 ASCII 字符
- 因此它可以表示:
- 英文字符
- 带重音字母
- 中文、日文、韩文字符
- emoji
- 零宽空格等特殊字符
需要注意:
- Rust 的
char对应的是单个 Unicode 标量值 - 它不一定等同于人类直觉上的“一个字符”
3.4 复合类型
Rust 有两种原生复合类型:
- 元组(tuple)
- 数组(array)
3.4.1 元组
元组用于把多个不同类型的值组合成一个整体,并且长度固定。
let tup: (i32, f64, u8) = (500, 6.4, 1);
元组特点:
- 长度固定
- 各元素类型可以不同
- 本身是一个整体值
元组解构
可以用模式匹配把元组拆开:
let tup = (500, 6.4, 1);
let (x, y, z) = tup;
这叫解构。
按索引访问元组
也可以用点号加索引访问:
let x = (500, 6.4, 1);
let a = x.0;
let b = x.1;
let c = x.2;
单元类型 ()
空元组叫做单元(unit),写作 ()。
它有两个重要语义:
- 表示“空值”
- 表示函数或表达式没有返回其他有意义的值
3.4.2 数组
数组用于保存多个相同类型的值,长度固定。
let a = [1, 2, 3, 4, 5];
数组特点:
- 每个元素类型必须相同
- 长度固定
- 常分配在栈上
- 适合元素数量不会变化的场景
数组类型写法
let a: [i32; 5] = [1, 2, 3, 4, 5];
其中:
i32是元素类型5是元素个数
简写初始化
let a = [3; 5];
等价于:
let a = [3, 3, 3, 3, 3];
访问数组元素
通过索引访问:
let a = [1, 2, 3, 4, 5];
let first = a[0];
let second = a[1];
数组越界
如果索引超出范围,Rust 会在运行时 panic!:
let a = [1, 2, 3, 4, 5];
let element = a[10]; // 运行时 panic
这是 Rust 内存安全设计的重要体现:
- Rust 不允许“错误索引继续访问未知内存”
- 越界会被检测并立即终止程序
- 从而避免底层语言中常见的非法内存访问问题
3.5 数组与 Vec 的区别
本章只简单点到,但理解很重要:
- 数组:长度固定
Vec:长度可变
因此:
- 明确知道元素数量不会变时,数组更合适
- 不确定长度或需要动态扩容时,更常用
Vec
3.6 本节要点
- Rust 是静态类型语言,必要时必须写类型注解。
- 标量类型包括整数、浮点数、布尔、字符。
char是单个 Unicode 标量值,远比 ASCII 更广。- 复合类型包括元组和数组。
- 数组越界会在运行时触发
panic!,这是安全保障而不是限制。
4. 函数
4.1 函数是 Rust 程序组织代码的核心方式
Rust 使用 fn 定义函数。main 是可执行程序的入口函数。
fn main() {
println!("Hello, world!");
another_function();
}
fn another_function() {
println!("Another function.");
}
4.2 命名风格:snake_case
Rust 的函数名和变量名通常使用 snake case:
- 全部小写
- 单词之间用下划线连接
例如:
mainanother_functionprint_labeled_measurement
4.3 函数定义位置不要求在调用之前
Rust 不要求函数必须先定义后调用。只要函数在当前可见作用域内即可。
这说明 Rust 在函数解析上不是逐行“看到才认识”,而是整体理解作用域结构。
4.4 参数
函数可以有参数,参数是函数签名的一部分。
fn another_function(x: i32) {
println!("The value of x is: {x}");
}
调用:
another_function(5);
其中:
x是参数名i32是参数类型5是调用时传入的具体值
4.5 Rust 要求函数参数必须写类型
Rust 要求在函数签名中显式写出每个参数的类型。这一要求的价值在于:
- 函数接口清晰
- 编译器更容易给出精确错误信息
- 阅读者无需跳到函数体内部猜测参数类型
4.6 多参数函数
多个参数之间用逗号分隔:
fn print_labeled_measurement(value: i32, unit_label: char) {
println!("The measurement is: {value}{unit_label}");
}
4.7 语句与表达式
这是 Rust 语义中非常关键的一点。
语句(statement)
语句执行一个动作,但不返回值。
例如:
let y = 6;
它是一个语句。
表达式(expression)
表达式会计算并产生一个值。
例如:
65 + 6- 函数调用
- 宏调用
- 代码块
Rust 是表达式语言
这意味着 Rust 中很多结构都会产生值,因此“值流动”是 Rust 程序组织的重要基础。
4.8 let 语句不返回值
let x = (let y = 6); // 错误
原因:
let y = 6是语句- 语句不返回值
- 因而不能把它绑定给
x
4.9 代码块也是表达式
let y = {
let x = 3;
x + 1
};
这个代码块的值是最后那一行表达式 x + 1 的值,也就是 4。
因此:
y最终绑定到4- 花括号块本身就是一个表达式
4.10 分号的语义非常重要
在 Rust 中:
- 表达式末尾不加分号,表示“我要这个值”
- 表达式末尾加分号,就把它变成语句,值被丢弃
这会直接影响函数返回值与代码块求值结果。
4.11 有返回值的函数
Rust 函数可以返回值,返回类型写在 -> 后面:
fn five() -> i32 {
5
}
这里函数返回值是最后一个表达式 5 的结果。
调用:
let x = five();
等价于:
let x = 5;
4.12 隐式返回
Rust 中大多数函数并不显式写 return,而是直接让函数体最后一个表达式作为返回值。
例如:
fn plus_one(x: i32) -> i32 {
x + 1
}
4.13 分号导致返回值消失
fn plus_one(x: i32) -> i32 {
x + 1;
}
这会报错,因为:
x + 1原本是表达式,可返回i32- 加上分号后变成语句
- 语句不返回
i32 - 函数体末尾就只剩下单元值
() - 与函数声明的
-> i32不匹配
这一点是 Rust 初学阶段最重要的细节之一。
4.14 本节要点
- Rust 使用
fn定义函数。 - 函数名通常使用 snake_case。
- 参数类型必须写清楚。
- Rust 区分语句和表达式。
- 代码块本身也能产生值。
- 函数常通过最后一个无分号表达式隐式返回值。
- 多写一个分号,常常就是“值消失”的根源。
5. 注释
5.1 普通注释语法
Rust 中最常用的注释是 //,它从当前位置一直持续到行尾。
// hello, world
5.2 多行注释的常见写法
Rust 习惯上不是把大段解释都塞进复杂块注释,而是每一行都写 //:
// This is line 1.
// This is line 2.
// This is line 3.
5.3 行尾注释
注释也可以放在代码行尾:
let lucky_number = 7; // I'm feeling lucky today
5.4 更推荐的写法
更常见、也通常更清晰的方式,是把注释放在它解释的代码前一行:
// I'm feeling lucky today
let lucky_number = 7;
这样有两个优点:
- 代码行本身更整洁
- 注释和代码结构更容易按块阅读
5.5 注释的本质
注释是给人看的,不是给编译器看的。编译器会忽略注释内容。
因此,注释的目标不应是“翻译代码表面动作”,而应当解释:
- 为什么这样写
- 这样做的意图是什么
- 哪些地方容易误解
5.6 文档注释
本章只提到普通注释,但顺带提醒:Rust 还有一种专门用于文档生成的注释形式,即文档注释,后续在 crate 发布与文档生成章节中再深入讨论。
6. 控制流
Rust 的控制流核心包括:
ifelse ifelseloopwhileforbreakcontinue
6.1 if 表达式
if 用于根据条件决定是否执行某段代码。
let number = 3;
if number < 5 {
println!("condition was true");
} else {
println!("condition was false");
}
6.2 if 条件必须是 bool
Rust 不会像某些语言那样把整数自动转换为布尔值。
错误写法:
if number {
println!("number was three");
}
正确写法必须显式构造布尔表达式:
if number != 0 {
println!("number was something other than zero");
}
这体现了 Rust 的一个重要原则:
- 不做隐式、模糊、可能误导程序员的类型转换。
6.3 else if 处理多重分支
if number % 4 == 0 {
println!("number is divisible by 4");
} else if number % 3 == 0 {
println!("number is divisible by 3");
} else if number % 2 == 0 {
println!("number is divisible by 2");
} else {
println!("number is not divisible by 4, 3, or 2");
}
这里的规则是:
- 从上往下依次判断
- 执行第一个为真的分支
- 一旦匹配成功,后续分支不再检查
如果 else if 过多,代码会变乱。此时通常应该考虑用 match 重构。
6.4 if 是表达式,不只是语句
Rust 中 if 可以产生值,因此能直接出现在 let 右侧:
let condition = true;
let number = if condition { 5 } else { 6 };
这说明:
if整体求值为一个结果- 该结果再绑定给变量
6.5 if 各分支返回值类型必须一致
错误示例:
let number = if condition { 5 } else { "six" };
原因:
if分支返回整数else分支返回字符串切片- 变量只能有一个确定类型
- Rust 要求在编译期就能确定这个类型
因此,作为表达式的 if,所有可能分支的结果类型必须兼容。
6.6 循环概览
Rust 提供三种循环:
loopwhilefor
6.7 loop:无条件循环
loop 表示一直循环,直到显式终止。
loop {
println!("again!");
}
这种循环若没有 break,就是无限循环。
6.8 break 与 continue
break
- 立即退出当前循环
continue
- 跳过当前这轮循环后续代码
- 直接进入下一轮迭代
这两个关键字在处理复杂循环逻辑时极其重要。
6.9 loop 可以返回值
这是 Rust 很有特色的设计。
let mut counter = 0;
let result = loop {
counter += 1;
if counter == 10 {
break counter * 2;
}
};
这里:
break counter * 2不只是退出循环- 它还把
counter * 2作为整个loop表达式的值返回 - 最终
result == 20
这再次说明:Rust 中很多控制结构都是表达式。
6.10 break 与 return 的区别
break:退出当前循环return:退出当前函数
两者作用范围完全不同,不能混淆。
6.11 循环标签(loop label)
当出现嵌套循环时,break 和 continue 默认只作用于最内层循环。为了显式指定作用对象,Rust 支持循环标签。
'counting_up: loop {
loop {
break 'counting_up;
}
}
特点:
- 标签以单引号开头
- 可与
break或continue搭配使用 - 用于跳出或继续指定的外层循环
在多层嵌套中,这能显著减少歧义。
6.12 while:条件循环
如果循环是否继续取决于某个布尔条件,用 while 更自然。
let mut number = 3;
while number != 0 {
println!("{number}!");
number -= 1;
}
println!("LIFTOFF!!!");
while 的特点是:
- 条件为真则继续循环
- 条件为假则退出
- 比手写
loop + if + break更清晰
6.13 while 遍历集合的问题
例如:
let a = [10, 20, 30, 40, 50];
let mut index = 0;
while index < 5 {
println!("the value is: {}", a[index]);
index += 1;
}
虽然能工作,但存在两个问题:
- 容易写错边界条件,导致越界或遗漏元素。
- 编译器需要在每次索引访问时做边界检查,写法不够自然。
因此它不算 Rust 风格中最推荐的遍历方式。
6.14 for:遍历集合的首选方式
Rust 更推荐使用 for 遍历集合:
let a = [10, 20, 30, 40, 50];
for element in a {
println!("the value is: {element}");
}
优点:
- 更简洁
- 更安全
- 不需要手动维护索引
- 不容易产生越界与漏遍历错误
因此,在遍历数组、切片、迭代器等场景中,for 往往是最符合 Rust 风格的写法。
6.15 用 for 处理倒计时
for number in (1..4).rev() {
println!("{number}!");
}
println!("LIFTOFF!!!");
这里涉及两个概念:
1..4:生成从 1 到 3 的范围(不含 4).rev():将其反转
于是输出顺序为 3, 2, 1。
这比手写可变变量和 while 通常更优雅。
6.16 本节要点
if条件必须显式是bool。if是表达式,因此可以用于赋值。if各分支返回值类型必须一致。loop是无条件循环,适合需要手动控制退出条件的场景。loop可以通过break 值返回结果。while适合“条件成立就继续”的循环。for是遍历集合和范围时最推荐的方式。- 嵌套循环中可用标签精确控制
break和continue的目标。
7. 第 3 章核心观念总表
7.1 变量层面
- 默认不可变是 Rust 的基础设计取向。
mut是显式可变绑定。const是编译期常量,要求类型明确。- 遮蔽是“重新绑定”,不是“修改原变量”。
7.2 类型层面
- Rust 是静态类型语言。
- 编译器能推断时可省略;不能推断时必须显式标注。
- 标量类型关注“单值”,复合类型关注“组合”。
- 数组固定长度、元素同型;元组固定长度、元素可异型。
7.3 语义层面
- Rust 严格区分语句和表达式。
- 大量控制结构都能产生值。
- 分号不是“句末装饰”,而是“是否保留值”的语义信号。
7.4 控制流层面
- 条件判断必须显式布尔化。
- 多分支可用
else if,但过多时应考虑更强的匹配结构。 - 循环首选符合语义的结构:
loop:手动控制型while:条件驱动型for:遍历驱动型
8. 易错点与学习提醒
8.1 误把“不可变”理解为“值永远无法变化”
应理解为:当前绑定默认不可修改,而不是说程序里永远不能出现新值。同名遮蔽就能产生新绑定。
8.2 混淆 mut 和遮蔽
mut:同一绑定被重新赋值- 遮蔽:创建新绑定,可变更类型
8.3 忘记 const 必须写类型
这是 const 与普通 let 的关键差异之一。
8.4 忘记 parse() 常需要类型注解
编译器不知道你要解析成 u32、i32 还是其他类型时,必须显式写出。
8.5 误把 char 当作“任意长度字符”
char 只表示一个 Unicode 标量值,不等于任意用户感知字符序列。
8.6 忘记数组长度固定
数组不是动态容器,需要动态长度时通常应使用 Vec。
8.7 忽视分号的语义影响
在 Rust 中,多一个分号往往就会让原本的表达式变成语句,进而导致:
- 代码块不再返回值
- 函数返回类型不匹配
- 难以理解的
()错误
8.8 用 while + 索引 遍历集合
虽然可行,但不够安全优雅。遍历集合时应优先考虑 for。
9. 本章总结
第 3 章的价值不在于“介绍一些基础语法”,而在于建立 Rust 的基础语义思维方式:
- 默认不可变:Rust 倾向于通过约束降低错误空间。
- 类型清晰:编译期掌握足够信息,才能提供更强保障。
- 表达式导向:值如何流动,是理解 Rust 代码结构的关键。
- 控制流有类型约束:不是“能跑就行”,而是“语义明确、类型一致、边界清楚”。
- 安全是默认原则:无论是数组越界检查,还是布尔条件的严格要求,都体现了 Rust 用编译器和运行时检查主动阻断错误的设计哲学。
学完本章后,应该达到的目标不是只记住某几个语法点,而是形成下面这套判断:
- 什么时候应该用不可变绑定,什么时候该显式加
mut - 什么时候该用常量,什么时候该用变量
- 什么时候该写类型注解
- 什么时候代码块在“产生值”,什么时候它只是“执行动作”
- 什么时候该用
loop、while、for - 为什么 Rust 经常拒绝那些在其他语言里“看起来也能凑合运行”的写法
这些观念会直接影响后续对所有权、借用、模式匹配、错误处理和泛型的理解。
10. 章末练习建议
根据本章内容,可以用以下小项目巩固概念:
-
摄氏度与华氏度相互转换程序
- 练习变量、函数、数值运算、输入输出、类型转换
-
生成第
n个斐波那契数- 练习循环、可变变量、函数设计、整数类型选择
-
打印《The Twelve Days of Christmas》歌词
- 练习字符串、数组、
for循环、嵌套控制流
- 练习字符串、数组、
完成这些练习时,建议重点关注:
- 是否合理使用了不可变与可变绑定
- 是否在需要时显式标注类型
- 是否理解了表达式与返回值
- 是否优先使用了更符合 Rust 风格的控制流结构