📚 Rust 课程系列

  1. 课程概览
  2. 基础语法(一):变量、数据类型与字符串
    • 基础语法(二):运算符、表达式与控制流(本文)
  3. 函数与输入输出
  4. 所有权、借用与生命周期
  5. 结构体、枚举与模式匹配
  6. 类型系统:泛型、trait 与多态
  7. 集合与容器
  8. 错误处理与 Panic 恢复
  9. 模块、属性与宏
  10. 智能指针、迭代器与闭包
  11. 并发与异步编程
  12. Unsafe Rust 与常用 trait 详解
  13. 工具链、Cargo 与外部 crate
  14. 最佳实践、性能与调试

承接基础语法(一)中变量与数据类型的基础,本篇聚焦 Rust 语法的另一条主线:运算符、Range 范围语法,以及贯穿全篇的面向表达式(expression-oriented)语法哲学与控制流。

面向表达式是贯穿 Rust 的语法主线–它继承自 ML/OCaml 的函数式血统,让 ifmatch、代码块乃至 loop 都能"求值"为一个值。这一设计不仅消除了三元运算符、减少了临时变量,更让 Rust 的类型系统得以在表达式的每一个位置精确追踪值的类型与所有权。理解了"分号 = 丢弃值"这一规则,你就掌握了 Rust 语法的灵魂。

本篇还将详解控制流的完整图景:if / while / for / loop 四种循环、if let / while let / let else 三种模式匹配快捷语法、标签跳转,以及 match 的守卫、绑定、范围、解构等高级模式。最后以命名规范速查收尾。

运算符一览

运算符分类速查

类别运算符说明
算术+ - * / %% 为取余;整数除法向零取整
比较== != < <= > >=返回 bool,不支持链式比较
逻辑&& || !短路求值,仅接受 bool
位运算& | ^ << >>整数逐位操作;! 也是按位取反
复合赋值+= -= <<=所有算术/位运算符均支持
引用/解引用& &mut *借用与解引用
其他. [] ..字段访问、索引、Range
1
2
3
4
5
6
7
8
9
10
11
let a = 7 / 2;          // 3:整数除法向零取整
let b = -7 / 2; // -3:向零取整,不是向下取整
let c = -7 % 2; // -1:余数符号跟随被除数
let d = 7.0 / 2.0; // 3.5:浮点除法

let e = 0b1100 & 0b1010; // 0b1000:按位与
let f = 1u8 << 4; // 16:左移 4 位

// 短路求值:左侧为 false 时右侧不执行
let v = vec![1, 2, 3];
let ok = !v.is_empty() && v[0] > 0;

⚠️ 注意:整数除法向零取整(-7 / 2 == -3),与 Python 的向下取整(-7 // 2 == -4)不同,处理负数时要留意;需要向下取整用 div_euclid / rem_euclid 方法。Rust 没有 ++/-- 运算符,统一用 x += 1–避免前置/后置语义的歧义。

💡 提示!bool 是逻辑非,对整数是按位取反;&/| 用于 bool 时不短路(两侧都会求值)。这些运算符都可被自定义类型重载–实现 std::ops 中对应的 trait(如 AddBitOr)即可。

运算符重载与 std::ops trait

Rust 的运算符并非语言内建魔法,而是 trait 的语法糖。当你写 a + b 时,编译器实际调用 a.add(b);写 a == b 时,调用 a.eq(b)。自定义类型只要实现对应 trait,就能"重载"运算符。这一设计让运算符的语义可扩展,同时保持类型系统的严格检查。

运算符与 trait 的对应关系:

运算符trait方法示例
+std::ops::Addadda + b
-std::ops::Subsuba - b
*std::ops::Mulmula * b
/std::ops::Divdiva / b
%std::ops::Remrema % b
-(一元)std::ops::Negneg-a
!(按位)std::ops::Notnot!a
&std::ops::BitAndbitanda & b
|std::ops::BitOrbitora | b
^std::ops::BitXorbitxora ^ b
<<std::ops::Shlshla << b
>>std::ops::Shrshra >> b
*(解引用)std::ops::Derefderef*a
[]std::ops::Indexindexa[b]
== !=std::cmp::PartialEqeq nea == b
< <= > >=std::cmp::PartialOrdlt lea < b

下面为自定义结构体实现 Add trait,让 + 运算符作用于 Point 类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
use std::ops::Add;

#[derive(Debug, Clone, Copy)]
struct Point { x: i32, y: i32 }

impl Add for Point {
type Output = Point;
fn add(self, rhs: Point) -> Point {
Point {
x: self.x + rhs.x,
y: self.y + rhs.y,
}
}
}

let p = Point { x: 1, y: 2 };
let q = Point { x: 3, y: 4 };
let r = p + q;
// r = Point { x: 4, y: 6 }

type Output = Point 指定加法结果的类型–运算符 trait 使用关联类型而非泛型参数,这意味着 Point + Point 的结果类型是唯一的。如果需要 Point + i32 这样的混合运算,可以再实现一个 impl Add<i32> for Point

💡 提示:复合赋值运算符(如 +=)对应 AddAssign 等 trait。但实现了 Add 并不自动获得 +=–需要单独实现 AddAssign。不过对于 #[derive(Copy)] 的简单类型,编译器有时会自动处理。

比较运算符与 PartialEq / PartialOrd

比较运算符 ==!= 依赖 PartialEq trait,而 <<=>>= 依赖 PartialOrd trait。这两个 trait 的"Partial"前缀暗示了一个重要事实:并非所有类型都能全序比较。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// derive 自动实现比较 trait
#[derive(PartialEq, PartialOrd)]
struct Score(f64);

let a = Score(1.0);
let b = Score(2.0);
println!("{}", a < b); // true
println!("{}", a == b); // false

// f32 / f64 只实现 PartialOrd,没有 Ord
// 因为 NaN != NaN,全序关系不成立
let nan = f64::NAN;
println!("{}", nan == nan); // false
println!("{}", nan < 0.0); // false
println!("{}", nan > 0.0); // false

// 需要 Ord 的场景(如排序)会编译报错
// let mut nums = vec![1.0, f64::NAN, 2.0];
// nums.sort(); // ❌ f64 未实现 Ord

PartialOrdOrd 的区别在于:Ord 要求全序关系(任意两元素可比),而 PartialOrd 允许存在不可比的元素对。f64 因 NaN 的存在只能实现 PartialOrd。如果自定义类型的比较逻辑天然全序,应该实现 Ord(通常通过 #[derive(Ord)] 同时派生 Eq + Ord)。

🔄 对比:Java 中所有对象都有 equals()compareTo(),即使是 NaN 也"被迫"有比较结果。Rust 选择在类型系统层面区分"部分可比"与"全序可比",让排序、BTreeMap 等需要全序的场景在编译期就能拒绝 f64 这样的类型。

位运算细节

位运算作用于整数类型的二进制补码表示。理解补码是正确使用位运算的前提:

1
2
3
4
5
6
7
8
9
10
11
12
// 补码表示:正数原样,负数取反加一
let x: i8 = -1;
// -1 的补码:0b1111_1111(全 1)
println!("{:08b}", x); // 11111111

let y: i8 = -128;
// -128 的补码:0b1000_0000
println!("{:08b}", y); // 10000000

// 按位取反:~x = -x - 1
let z = !x; // !(-1) = 0
println!("{}", z); // 0

移位运算的溢出行为需要特别留意:

1
2
3
4
5
6
7
8
9
10
11
12
13
let x: u8 = 200;
// 左移溢出:debug panic,release 回绕
let _y = x << 1; // 400 超出 u8 范围

// 移位量 >= 位宽:debug panic
let _z = 1u8 << 8; // 8 >= 8 位

// 右移不会溢出(高位补 0 或符号位)
let _w = 200u8 >> 1; // 100,安全

// 有符号数右移:算术右移(补符号位)
let n: i8 = -8;
let _m = n >> 1; // -4:补符号位 1

⚠️ 注意<< 在 debug 模式下,若移位导致溢出(丢失非零位)会 panic;release 模式下回绕。移位量(右操作数)大于等于类型位宽时,debug 同样 panic。右移 >> 对无符号数补 0(逻辑右移),对有符号数补符号位(算术右移)–这与 C/C++ 的实现定义行为不同,Rust 的规则是确定的。

解引用、? 与赋值语句

三个需要单独说明的运算符:

解引用 *:取出引用指向的值。它依赖 Deref trait,标准库的 BoxRcString 等智能指针类型都实现了 Deref,使得 * 可以穿透多层引用。Deref 还支持隐式解引用转换(deref coercion)–&String 可以自动转为 &str。详见第 9 章:智能指针、迭代器与闭包

1
2
3
4
5
6
7
8
let x = 5;
let r = &x;
assert_eq!(*r, 5); // *r 取出 r 指向的值

// Deref 隐式转换
let s = String::from("hi");
let _bytes: &[u8] = s.as_bytes();
let _str: &str = &s; // &String -> &str

? 运算符:错误传播的语法糖。expr?exprErr 时提前返回错误,为 Ok 时解包值。它让错误处理代码大幅简化,详见第 7 章:错误处理与 Panic 恢复

1
2
3
4
5
fn read_config(path: &str)
-> Result<String, std::io::Error> {
let content = std::fs::read_to_string(path)?;
Ok(content) // ? 失败时提前返回 Err
}

赋值是语句:在 Rust 中,赋值 = 是语句而非表达式,求值为 ()。这与 C/C++ 截然不同:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 赋值返回 (),不是被赋的值
let mut x = 5;
let y = (x = 6); // y 的类型是 ()
// x = 6, y = ()

// 这杜绝了 if (x = 1) 类经典错误
// C/C++ 中:if (x = 1) 总为真(赋值后 x=1)
// Rust 中:if x = 1 { } 编译错误
// 因为 x = 1 返回 (),不是 bool

// 复合赋值同理
let mut n = 5;
n += 3; // 语句,返回 ()
// let m = (n += 3); // m: ()

🔄 对比:C/C++ 中赋值是表达式(a = b = c 合法且常见),Rust 中赋值是语句返回 ()let x = (y = 6); 虽能编译但 x 的类型是 (),几乎总是 bug。这一设计从语言层面杜绝了 if (x = 1) 这类"赋值误写为比较"的经典错误,是 Rust 追求安全的一个缩影。

Range(范围)语法

Range 是 Rust 中表示序列范围的重要语法,广泛用于循环迭代、切片操作和模式匹配。

基本语法

1
2
for i in 1..5 { }   // 半开区间:1, 2, 3, 4
for i in 1..=5 { } // 闭区间:1, 2, 3, 4, 5

切片中的 Range

1
2
3
4
let arr = [10, 20, 30, 40, 50];
let a = &arr[..3]; // 从起点:[10, 20, 30]
let b = &arr[2..]; // 到终点:[30, 40, 50]
let c = &arr[..]; // 全范围:[10, 20, 30, 40, 50]

Range 类型

语法类型说明
start..endRange<T>半开区间
start..=endRangeInclusive<T>闭区间
start..RangeFrom<T>从 start 到无限
..endRangeTo<T>从起点到 end
..=endRangeToInclusive<T>从起点到 end(包含)
..RangeFull全范围
1
2
3
4
5
6
7
8
9
10
11
use std::ops::{
Range, RangeFrom, RangeFull, RangeInclusive, RangeTo,
RangeToInclusive,
};

let r1: Range<i32> = 1..5;
let r2: RangeInclusive<i32> = 1..=5;
let r3: RangeFrom<i32> = 3..;
let r4: RangeTo<i32> = ..5;
let r5: RangeToInclusive<i32> = ..=5;
let r6: RangeFull = ..;

Range 作为迭代器

RangeRangeInclusive 都实现了 Iterator trait,这意味着它们可以直接用于 for 循环和 collect,且所有迭代器适配器方法都可用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 直接 collect
let v1: Vec<i32> = (1..5).collect();
let v2: Vec<i32> = (1..=5).collect();
// v1=[1,2,3,4], v2=[1,2,3,4,5]

// 迭代器方法链
let sum: i32 = (1..=100).sum(); // 5050
let product: i32 = (1..=5).product(); // 120
let max: Option<i32> = (1..5).max(); // Some(4)
let min: Option<i32> = (1..5).min(); // Some(1)

// 配合 filter / map
let evens: Vec<i32> = (1..=10)
.filter(|x| x % 2 == 0)
.collect(); // [2, 4, 6, 8, 10]

💡 提示Range 实现的是 Iterator 而非 IntoIterator–它本身就是迭代器。这意味着 for i in 1..5 中的 1..5 直接作为迭代器使用,无需额外转换。Range 的迭代是零成本的:每次 next() 只是递增一个整数,编译后与手写的 while 循环等价。

RangeInclusive 的注意点

RangeInclusive 有一个内部 exhausted 标志位,用于记录是否已经迭代到终点。这带来两个容易踩坑的行为:

is_empty() 行为反直觉:在迭代之前,is_empty() 总是返回 false,即使 start > end(如 5..=1)。只有迭代完成后,is_empty() 才会返回正确结果。

1
2
3
4
5
6
7
8
// 5..=1 实际为空(start > end)
let r = 5..=1;
// 但 is_empty() 在迭代前返回 false
println!("{}", r.is_empty()); // false!

// 对比 Range 的行为
let r2: std::ops::Range<i32> = 5..1;
println!("{}", r2.is_empty()); // true(立即正确)

这是因为 RangeInclusive 在迭代前无法确定是否为空–它需要在第一次 next() 调用时检查 start > end,并设置 exhausted 标志。

消耗后不可复用RangeInclusive 没有实现 Copy(因为有 exhausted 可变标志位),迭代消耗后不可复用:

1
2
3
4
5
6
7
8
9
// RangeInclusive:非 Copy,消耗后不可复用
let r = 1..=3;
for i in r { print!("{}", i); } // 123
// for i in r { } // ❌ r 已被移动

// 对比 Range:是 Copy,可复用
let r = 1..3;
for i in r { print!("{}", i); } // 12
for i in r { print!("{}", i); } // 12 ✅

⚠️ 注意:如果需要在多处复用 RangeInclusive,用 r.clone() 显式克隆,或改用 Range(半开区间)+ 手动处理边界。在大多数场景下,1..51..=5 更方便,因为 RangeCopy 的。

rev() / step_by() 返回新迭代器

rev()step_by() 是迭代器适配器方法,它们消费原迭代器并返回一个新的包装类型,而非原地修改。理解这一点有助于避免所有权相关的困惑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// rev() 返回 Rev<Range> 新迭代器
let v: Vec<i32> = (1..5).rev().collect();
// [4, 3, 2, 1]

// step_by() 返回 StepBy<Range> 新迭代器
let v: Vec<i32> = (0..10).step_by(2).collect();
// [0, 2, 4, 6, 8]

// 适配器可链式调用
let v: Vec<i32> = (1..10)
.step_by(2)
.rev()
.collect();
// [9, 7, 5, 3, 1]

// 原始 range 未被修改(适配器消费的是副本)
let r = 1..5;
let v: Vec<i32> = r.clone().rev().collect();
let v2: Vec<i32> = r.collect();
// v=[4,3,2,1], v2=[1,2,3,4]

💡 提示:所有迭代器适配器(mapfiltertakeskip 等)都遵循同样的模式:消费 self,返回新类型。这是 Rust 零成本抽象的体现–适配器层层嵌套,但编译后会被内联优化为等价的循环,无运行时开销。

Range 在模式匹配中的应用

1
2
3
4
5
6
let x = 5;
match x {
1..=10 => println!("1到10"),
11..=100 => println!("11到100"),
_ => println!("其他"),
}

⚠️ 注意:切片越界会 panic,用 get() 安全访问:arr.get(1..5) 返回 Option。半开区间 start >= end 时 range 为空,循环体不执行。浮点数 range 只有闭区间 ..= 可迭代,但不推荐(精度问题)。

Range 与切片语法(进阶)

🔬 进阶:Range 类型实现了 SliceIndex trait 的类型都可以用作 [] 索引参数–Range 类型正是如此。自定义类型只要实现 SliceIndex,也能用于切片索引,这是 Rust 索引语法可扩展性的基础。

表达式与语句:Rust 的语法基石

Rust 是面向表达式(expression-oriented)的语言–这一设计继承自 ML/OCaml 的函数式血统,是理解 Rust 语法的第一把钥匙。与 C/Java/Go 等"语句为中心"的语言相比,Rust 的语法哲学可以浓缩为一句话:

几乎所有带花括号的构造都是表达式;分号的意思是"我不需要这个值"。

两者的严格定义:

  • 语句(Statement):执行动作,不返回值(求值为 ()),以分号结尾
  • 表达式(Expression):求值为一个值,可嵌套在任何需要值的位置

语句只有三种

1
2
3
4
5
6
7
8
9
// 1. let 绑定语句(整条是语句,不是表达式)
let x = 5;

// 2. 项(item)声明:fn、struct、impl、use 等
fn foo() {}

// 3. 表达式语句:表达式 + 分号,值被丢弃
x + 1; // 计算了但没人接收,直接丢弃
println!("hi"); // 宏调用本身也是表达式,求值为 ()

几乎所有构造都能求值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// if 是表达式 -> Rust 不需要三元运算符
let abs = if n >= 0 { n } else { -n };

// match 是表达式 -> 穷尽检查 + 直接产出值
let desc = match score {
90..=100 => "优秀",
60..=89 => "及格",
_ => "不及格",
};

// 代码块 {} 是表达式:值 = 最后一个表达式
let area = {
let r = 2.0;
std::f64::consts::PI * r * r // 无分号 -> 块的值
};

// loop 可以通过 break 带出值
let mut counter = 0;
let result = loop {
counter += 1;
if counter == 10 { break counter * 2; } // result = 20
};

表达式求值时的 move / copy 语义

表达式不只是"计算出一个值"–在 Rust 中,表达式的求值涉及所有权转移。这是 Rust 表达式系统与函数式语言的关键差异:表达式求值时,值要么被复制(Copy 类型),要么被移动(非 Copy 类型)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Copy 类型:表达式中复制,原变量仍可用
let a = 5;
let b = a + a; // a 被复制两次
println!("{}", a); // ✅ 仍可用,输出 5

// 非 Copy 类型:表达式中移动
let s = String::from("hi");
let t = s + "!"; // s 被移动到 + 运算
// println!("{}", s); // ❌ s 已被移动

// 引用本身是 Copy
let s = String::from("hi");
let r = &s;
let r2 = r; // r 被复制,不是移动
println!("{} {}", r, r2); // ✅ 都可用

// 函数实参也遵循同样的规则
fn take(s: String) {}
let s = String::from("hi");
take(s); // s 被移动
// println!("{}", s); // ❌ 已移动

这一规则延伸到所有表达式位置:运算符操作数、函数实参、数组元素、match 分支返回值等。理解这一点是避免"值在表达式中间被意外移动"这类编译错误的关键。

💡 提示:如果需要在表达式中"使用"非 Copy 值但不转移所有权,传引用(&s&mut s)。引用是 Copy 类型,传递引用不会移动原值。详见所有权、借用与生命周期

块表达式与临时变量生命周期

代码块 {} 是表达式,其值为最后一个表达式的值。块内创建的临时变量在块结束时销毁,但块表达式的值会被"带出"块外:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 块内的临时变量在块结束时销毁
let avg = {
let temp = vec![1, 2, 3, 4];
let sum: i32 = temp.iter().sum();
sum as f64 / temp.len() as f64
};
// temp 在块外不可访问,已销毁
// avg = 2.5

// Copy 类型:值被复制出块
let n = { 5 + 3 }; // n = 8

// 非 Copy 类型:值被移出块
let owned: String = {
let s = String::from("临时");
s // s 被移出块,不会被 drop
};
// owned 拥有字符串数据

// 如果块的最后一行有分号,值为 ()
let nothing: () = {
let s = String::from("hi");
s; // s 被移动并立即 drop,块值为 ()
};

块表达式的这一特性让 Rust 的资源管理非常精确:你可以用块来限定临时变量的作用域,确保资源在正确时机释放,同时将计算结果以值的形式传出。

分支类型必须一致

ifmatchloop 作为表达式时,所有分支必须返回相同的类型,否则编译错误。这是类型系统对表达式完整性的要求:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// ✅ 分支类型一致
let x = if cond { 1 } else { 2 };

// ❌ 分支类型不一致:编译错误
// let x = if cond { 1 } else { "two" };
// error[E0308]: mismatched types
// expected integer, found &str

// match 同理:所有分支必须同类型
let s = match n {
0 => "零",
1 => "一",
_ => "多",
}; // 所有分支都是 &'static str

// if 分支有多个表达式时,最后一行决定类型
let x = if cond {
let temp = 5;
temp * 2 // 无分号:x 的值
} else {
0
};

⚠️ 注意if 表达式中如果没有 else 分支,其类型为 if cond { T } else { () }–即 else 隐式返回 ()。所以 let x = if cond { 5 };x 的类型是 i32(当 cond 为假时 x 未赋值–实际上这是编译错误,因为 i32() 不匹配)。正确的写法是 let x = if cond { Some(5) } else { None };

语句求值为 () 与值丢弃

分号将表达式转为语句,值被丢弃。这一规则看似简单,但理解它对写出正确的 Rust 代码至关重要:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 分号将表达式转为语句,值被丢弃
let x = {
let a = 5;
let b = 3;
a + b; // 8 被计算但丢弃
a * b // 15 成为块的值(无分号)
};
// x = 15

// 函数中的经典场景
fn compute() -> i32 {
let temp = 42;
temp + 8; // 50 被计算但丢弃
temp // 42 成为返回值
}
// compute() = 42

// 丢弃的值如果触发警告
let unused = 5;
// unused; // warning: unused value

宏调用作为表达式

宏调用(如 vec![]println!format!)在语法上是表达式,求值为宏展开后表达式的值。宏在编译期展开为普通代码,展开后的表达式参与类型推断:

1
2
3
4
5
6
7
8
9
10
// 宏调用求值为展开后的值
let v: Vec<i32> = vec![1, 2, 3];
let s: () = println!("hi"); // 求值为 ()
let msg: String = format!("answer: {}", 42);

// 宏展开后参与方法链
let len = vec![1, 2, 3].len(); // 3
let first = vec![1, 2, 3]
.iter()
.next(); // Some(&1)

宏与函数的区别在于:宏在编译期展开,可以接受任意数量的参数、生成任意代码;函数在运行期调用,参数类型固定。宏作为表达式时,其类型就是展开后代码的类型。

复杂表达式何时拆分

Rust 的方法链和表达式嵌套可以写出非常紧凑的代码,但过度紧凑会损害可读性。何时拆分是一个工程判断:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 方法链是惯用法,适度链式调用清晰
let v: Vec<i32> = (1..=10)
.filter(|x| x % 2 == 0)
.map(|x| x * x)
.collect();
// [4, 16, 36, 64, 100]

// 过度嵌套:难读
// let r = if let Some(v) = opt
// .filter(|&x| x > 0)
// .map(|x| x * 2)
// .filter(|&x| x < 100)
// { v } else { 0 };

// 拆分有助调试和可读性
let positive = opt.filter(|&x| x > 0);
let doubled = positive.map(|x| x * 2);
let small = doubled.filter(|&x| x < 100);
let r = if let Some(v) = small {
v
} else {
0
};

// 以下场景考虑拆分:
// 1. 中间结果需要复用
// 2. 需要调试中间值
// 3. 类型标注复杂
// 4. 链超过 3-4 步

💡 提示:Rust 社区倾向于"适度链式"–2-4 步的方法链通常比等价的循环更清晰,但超过 5 步或包含复杂闭包时应考虑拆分。clippyclippy::redundant_closure 等检查可以帮助发现可简化的表达式。

与 C / Java / Go 的角色对比

构造C / Java / GoRust
if语句(Java 需 ?: 三元)表达式,可直接赋值
switch / match语句,fall-through 易出错表达式,穷尽检查
代码块 { }语句组表达式,值为最后一行
循环语句loop 可用 break 带出值
赋值 =表达式(a = b = c 合法)语句,求值为 ()
函数体必须显式 return尾部表达式即返回值

分号规则与经典陷阱

分号是"表达式 -> 语句"的转换器。加上分号,值被丢弃,求值结果变为单元类型 ()

1
2
3
4
5
6
7
8
let y = {
let inner = 3;
inner + 1 // 无分号 -> 块的值是 4
};

let z: () = {
3 + 1; // 有分号 -> 语句,块求值为 ()
};

函数返回值的经典陷阱:

1
2
3
4
5
6
7
fn double(x: i32) -> i32 {
x * 2 // ✅ 无分号:尾部表达式 = 返回值
}

fn double_bad(x: i32) -> i32 {
x * 2; // ❌ 有分号:语句,函数体求值为 ()
} // 报错:mismatched types

💡 提示return 只用于提前返回;函数最后一行用尾部表达式是社区惯例。表达式化设计让代码自然减少临时变量与重复命名,也让方法链、闭包、? 运算符等函数式风格水到渠成。

🔄 对比:C/C++ 中赋值是表达式(a = b = c 合法),Rust 中赋值是语句,返回 ()let x = (y = 6); 虽能编译但 x 的类型是 (),几乎总是 bug。这一设计杜绝了 if (x = 1) 这类经典的"赋值误写为比较"错误。

⚠️ 注意let 右侧、函数实参、数组元素等"需要值"的位置只能放表达式–所以 let x = let y = 5; 是语法错误(let 是语句不是表达式)。反之,if/match 表达式出现在语句位置时末尾必须加分号,否则编译器会把它当作块的最后表达式。

控制流

if / else if / else

if 在 Rust 中是表达式(详见上文「表达式与语句」一节),可以直接出现在赋值右侧–这也是 Rust 不需要三元运算符的原因:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// if 是表达式,可直接赋值--无需三元运算符
let result = if number > 0 {
"positive"
} else {
"non-positive"
};

// if / else if / else 链
let grade = if score >= 90 {
"A"
} else if score >= 80 {
"B"
} else if score >= 60 {
"C"
} else {
"F"
};

// 分支类型必须统一
let x = if cond { 1 } else { 2 }; // ✅ i32
// let x = if cond { 1 } else { "a" }; // ❌ 类型不匹配

💡 提示:条件必须是 bool,Rust 不会像 C/C++ 那样将整数隐式转为布尔值。if 1 { } 是编译错误。if 作为表达式时,所有分支的类型必须一致,否则编译报错 mismatched types

while

while 是条件循环,当条件为 false 时退出。它适合不确定迭代次数的场景:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 基本用法
let mut n = 0;
while n < 5 {
println!("{}", n);
n += 1;
}

// 适合轮询、等待条件满足
let mut retries = 0;
while !is_ready() && retries < 3 {
retries += 1;
std::thread::sleep(
std::time::Duration::from_millis(100)
);
}

// while 求值为 (),不能带值返回
let result = while false {};
// result: ()

💡 提示:优先用 for + 迭代器替代 while + 索引–for 不会越界且更符合 Rust 惯例。while 适合条件不依赖集合迭代的场景(如轮询、状态机)。

while let

while letwhile 与模式匹配的结合:每次循环前先尝试匹配,匹配成功则执行循环体,匹配失败则退出循环。它特别适合"持续从迭代器或 Option 取值直到取完"的场景:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// while let:持续 pop 直到栈空
let mut stack = vec![1, 2, 3];
while let Some(top) = stack.pop() {
println!("{}", top);
}
// 输出:3, 2, 1

// 等价于 for 循环(但 for 更简洁)
for item in vec![1, 2, 3].into_iter().rev() {
println!("{}", item);
}

// while let 适合手动控制迭代的场景
let mut iter = vec![1, 2, 3, 4].into_iter();
while let Some(v) = iter.next() {
if v == 2 { continue; }
if v == 4 { break; }
println!("{}", v);
}
// 输出:1, 3

// 从流中持续读取
let mut lines = vec![
"hello".to_string(),
"world".to_string(),
];
while let Some(line) = lines.pop() {
println!("{}", line);
}

while letif let 的区别在于:if let 只匹配一次,while let 持续匹配直到失败。while let 本质上是 loop { match ... { Some(x) => ..., None => break } } 的语法糖。

for 与所有权

for 循环是 Rust 中最常用的循环形式,它通过三种迭代方法与集合交互,选择哪种方法直接影响所有权语义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
let v = vec![
String::from("a"),
String::from("b"),
];

// iter():不可变借用,v 仍可用
for s in v.iter() {
// s: &String
}
println!("{:?}", v);

// iter_mut():可变借用
for s in v.iter_mut() {
s.push_str("!");
// s: &mut String
}

// into_iter():消费 v
for s in v.into_iter() {
// s: String
}
// v 不可再用

三种迭代方式对比:

方法产出类型借用形式消费所有权典型用途
iter()&T不可变借用只读遍历
iter_mut()&mut T可变借用原地修改
into_iter()T转移所有权消费集合

⚠️ 注意for x in &collection 等价于 for x in collection.iter()for x in &mut collection 等价于 iter_mut()for x in collection 等价于 into_iter()。Rust 1.53+ 中 IntoIterator for &[T]for x in &vec 直接可用。选错迭代方式会导致编译错误,详见所有权、借用与生命周期

loop + break 带值

loop 是无限循环,通过 break 退出。break 可以携带一个值,使 loop 成为表达式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// loop + break 带值
let mut i = 0;
let result = loop {
i += 1;
if i >= 10 {
break i * 2; // 返回 20
}
};
// result = 20

// 适合复杂退出条件且需返回计算结果
let mut sum = 0;
let total = loop {
let next = fetch_next();
if next.is_none() {
break sum;
}
sum += next.unwrap();
};

标签循环

嵌套循环中,标签(label)可以精确控制 break / continue 作用于哪一层。标签以 'label: 形式声明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// break 跳出外层循环
'outer: for i in 0..3 {
for j in 0..3 {
if i == 1 && j == 1 {
break 'outer;
}
println!("({}, {})", i, j);
}
}
// 输出:(0,0) (0,1) (0,2) (1,0)

// continue 跳过外层当前迭代
'outer: for i in 0..3 {
for j in 0..3 {
if j == 1 {
continue 'outer; // 跳过 j=1,2,直接 i++
}
println!("({}, {})", i, j);
}
}
// 输出:(0,0) (1,0) (2,0)

// 标签 break 带值
let result = 'outer: loop {
for i in 0..100 {
if i == 42 {
break 'outer i; // 返回 42
}
}
break 0; // 默认返回
};
// result = 42

🔄 对比:Rust 的标签语法 'label: 类似 Go 的 label:,但 Rust 标签只能与 break/continue 配合,不能像 Go 那样 goto。标签名使用单引号前缀(与生命周期语法一致),这在视觉上与变量名区分开。

match 详解

match 是 Rust 最强大的控制流构造,支持丰富的模式匹配语法。完整详解见结构体、枚举与模式匹配,此处介绍核心模式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let x = 42;
match x {
// 1. 字面量模式
0 => println!("零"),
// 2. 多模式 |
1 | 2 | 3 => println!("小"),
// 3. 范围模式
4..=10 => println!("中"),
// 4. 绑定 @:绑定匹配的值
n @ 11..=99 => println!("n={}", n),
// 5. 守卫 if:附加条件
n if n > 99 => println!("大:{}", n),
// 6. 通配符(穷尽性兜底)
_ => println!("其他"),
}

守卫(guard):在模式后附加 if 条件,对匹配的值做额外检查:

1
2
3
4
5
6
7
8
9
10
let pair = (2, -3);
match pair {
(x, y) if x * y > 0 => {
println!("同号");
}
(x, y) if x + y == 0 => {
println!("相反数");
}
_ => println!("其他"),
}

绑定 @:将匹配到的值绑定到变量,同时测试范围:

1
2
3
4
5
6
7
8
let age = 25;
match age {
n @ 0..=12 => println!("儿童:{}", n),
n @ 13..=17 => println!("少年:{}", n),
n @ 18..=64 => println!("成人:{}", n),
n @ 65..=150 => println!("老年:{}", n),
_ => println!("异常"),
}

解构模式:对结构体、元组、枚举进行解构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 解构结构体
struct Point { x: i32, y: i32 }
let p = Point { x: 1, y: 0 };
match p {
Point { x: 0, .. } => println!("x=0"),
Point { x, y: 0 } => {
println!("y=0,x={}", x);
}
Point { x, y } => {
println!("{},{}", x, y);
}
}

// 解构元组
let (a, b, c) = (1, "hi", true);

// 解构枚举
enum Msg {
Quit,
Move { x: i32, y: i32 },
}
match Msg::Move { x: 1, y: 2 } {
Msg::Quit => println!("退出"),
Msg::Move { x, y } => {
println!("{},{}", x, y);
}
}

穷尽性要求match 必须覆盖所有可能的模式,否则编译错误。_ 通配符兜底,但过度使用 _ 会掩盖遗漏的分支:

1
2
3
4
5
6
7
8
9
10
11
12
13
// ❌ 未穷尽:编译错误
// match color {
// Red => ...,
// Green => ...,
// // 缺少 Blue
// }

// ✅ 用 _ 兜底
match color {
Red => "红",
Green => "绿",
_ => "其他",
}

💡 提示match 的穷尽性检查是 Rust 的一大安全特性–当你后续给枚举添加新变体时,所有 match 表达式会在编译期报错,提醒你处理新情况。尽量避免用 _ 兜底,显式列出所有变体更安全。

if let / while let / let else 决策对比

Rust 提供了三种 match 的快捷语法,各有适用场景。选择正确的构造让代码更简洁:

只关心一种匹配情况时,if let 比完整 match 更简洁:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
let opt: Option<i32> = Some(42);

// if let:匹配成功才执行分支
if let Some(v) = opt {
println!("值: {}", v);
}

// let...else:匹配失败时提前退出
let Some(v) = opt else {
panic!("没有值"); // else 分支必须发散
}; // panic!/return/break/continue
println!("v = {}", v); // v 已绑定成功

// while let:持续取值直到不匹配
let mut stack = vec![1, 2, 3];
while let Some(top) = stack.pop() {
println!("{}", top);
}

三种快捷语法的决策对比:

构造适用场景是否穷尽else 分支典型用法
if let只关心一种模式不匹配则跳过条件执行
while let持续取值直到不匹配不匹配则退出循环循环取值
let else匹配失败则提前退出必须发散(!)参数校验
match需要处理所有情况_ 兜底完整分支

选择决策流程:

flowchart TD
    A[需要模式匹配] --> B{需要处理所有分支?}
    B -->|是| C[用 match]
    B -->|否| D{匹配失败需提前退出?}
    D -->|是| E[用 let...else]
    D -->|否| F{需要循环取值?}
    F -->|是| G[用 while let]
    F -->|否| H[用 if let]

💡 提示let...else 的 else 分支必须是发散的(永不返回的类型 !),常用于函数开头的参数校验,避免多层嵌套。while let 则适合"持续取直到取完"的循环场景。完整的模式匹配语法见结构体、枚举与模式匹配

模式匹配在控制流中的应用

1
2
3
4
5
6
let pair = (0, -2);
match pair {
(0, y) => println!("x=0, y={}", y),
(x, 0) => println!("x={}, y=0", x),
_ => println!("其他情况"),
}

💡 提示match 是 Rust 最强大的控制流构造,支持解构、守卫、范围模式等。完整的模式匹配详解见结构体、枚举与模式匹配

命名规范速查

基本风格

风格示例
变量 / 函数 / 模块snake_caseuser_name, read_file
类型 / trait / 枚举CamelCaseUserProfile, Display
常量 / 静态变量SCREAMING_SNAKE_CASEMAX_RETRIES
生命周期短小写字母'a, 'input
crate / 包名snake_caseserde_json

前缀惯例

Rust 社区有一套广泛遵守的命名前缀惯例,让函数的返回类型和意图一目了然:

前缀用途返回类型示例
is_布尔状态查询boolis_empty()
has_拥有性查询boolhas_error()
can_能力查询boolcan_retry()
try_可能失败的操作Result / Optiontry_parse()
1
2
3
4
5
6
7
8
9
// 布尔查询:is_ / has_ / can_
fn is_empty(&self) -> bool { ... }
fn has_error(&self) -> bool { ... }
fn can_retry(&self) -> bool { ... }

// 可能失败的操作:try_
fn try_parse(s: &str) -> Option<i32> { ... }
fn try_connect(addr: &str)
-> Result<Conn, Error> { ... }

💡 提示:标准库中 is_ 前缀最为常见(is_emptyis_nanis_some)。try_ 前缀通常表示返回 ResultOption,但并非强制–如 try_into 返回 Result,而 try_get 在某些集合中返回 Option。关键是保持项目内一致性。

类型转换命名

类型转换方法的命名前缀遵循一套精确的约定,反映转换的开销和所有权语义:

前缀语义开销所有权示例
as_廉价转换,返回借用返回 &Tas_str()
to_昂贵转换,创建新对象产出新 Tto_string()
into_消费转换-消费 selfinto_vec()
1
2
3
4
5
6
7
8
9
10
11
// as_:廉价转换,返回借用
fn as_str(&self) -> &str { ... }
fn as_bytes(&self) -> &[u8] { ... }

// to_:昂贵转换,创建新对象
fn to_string(&self) -> String { ... }
fn to_vec(&self) -> Vec<T> { ... }

// into_:消费 self,转移所有权
fn into_vec(self) -> Vec<T> { ... }
fn into_string(self) -> String { ... }

🔄 对比as_ vs to_ 的区别类似于 &str vs Stringas_str() 返回借用(零拷贝),to_string() 分配新内存。into_ 消费原对象,适合不再需要原值的场景。这一命名约定让调用者从方法名就能推断性能特征。

getter 命名

Rust 的 getter 方法不加 get_ 前缀,直接用字段名作为方法名。这与 Java/Python 的惯例不同:

1
2
3
4
5
6
7
8
9
10
// ✅ Rust 风格:getter 不加 get_ 前缀
impl User {
fn name(&self) -> &str { &self.name }
fn age(&self) -> u32 { self.age }
fn email(&self) -> Option<&str> { ... }
}

// ❌ Java/Python 风格(clippy 会警告)
// fn get_name(&self) -> &str { ... }
// fn get_age(&self) -> u32 { ... }

🔄 对比:Java 惯例是 getName() / getAge(),Python 是 get_name()。Rust 省略 get_ 前缀的理由是:方法名已经表达了"获取"的语义,加前缀是冗余。clippywrong_self_convention 检查会对此发出警告。例外:当 getter 涉及计算或 IO 时,可以用 get_ 前缀区分(如 get_random_value)。

💡 提示:不符合命名规范时编译器会给出 non_snake_case 等警告(不是错误);cargo clippy 的检查更严格。统一的风格让阅读陌生 Rust 代码的成本显著降低。

🔄 对比:Rust 是少有的在编译器层面强制命名风格的主流语言(Go 用 gofmt 管格式但不管命名,Python 的 PEP 8 只是社区约定)。

小结

  • 运算符是 trait 的语法糖:a + b 调用 Add::adda == b 调用 PartialEq::eq;自定义类型实现对应 trait 即可重载运算符
  • 位运算作用于补码表示;<< 在 debug 模式下溢出会 panic,release 下回绕;赋值是语句返回 (),从语言层面杜绝 if (x = 1) 类错误
  • Range(Copy)与 RangeInclusive(非 Copy)的 is_empty() 行为和可复用性不同;rev() / step_by() 返回新迭代器而非原地修改
  • 面向表达式是语法主线if/match/块/loop 都能求值;语句只有 let 绑定、项声明、表达式语句三种
  • 表达式求值涉及 move / copy 语义:Copy 类型复制,非 Copy 类型移动;块表达式的值移出块外,临时变量在块结束时销毁
  • if/match/loop 各分支类型必须一致;分号是"表达式 -> 语句"的转换器,return 只用于提前返回
  • for 三种迭代方式(iter() / iter_mut() / into_iter())决定所有权语义;match 支持守卫、@ 绑定、范围、多模式、解构;if let / while let / let else 各有适用场景

下一篇将进入函数与输入输出,看看这些语法元素如何组织成可交互的程序。