Rust 基础语法(二):运算符、表达式与控制流
📚 Rust 课程系列
承接基础语法(一)中变量与数据类型的基础,本篇聚焦 Rust 语法的另一条主线:运算符、Range 范围语法,以及贯穿全篇的面向表达式(expression-oriented)语法哲学与控制流。
面向表达式是贯穿 Rust 的语法主线–它继承自 ML/OCaml 的函数式血统,让 if、match、代码块乃至 loop 都能"求值"为一个值。这一设计不仅消除了三元运算符、减少了临时变量,更让 Rust 的类型系统得以在表达式的每一个位置精确追踪值的类型与所有权。理解了"分号 = 丢弃值"这一规则,你就掌握了 Rust 语法的灵魂。
本篇还将详解控制流的完整图景:if / while / for / loop 四种循环、if let / while let / let else 三种模式匹配快捷语法、标签跳转,以及 match 的守卫、绑定、范围、解构等高级模式。最后以命名规范速查收尾。
运算符一览
运算符分类速查
| 类别 | 运算符 | 说明 |
|---|---|---|
| 算术 | + - * / % | % 为取余;整数除法向零取整 |
| 比较 | == != < <= > >= | 返回 bool,不支持链式比较 |
| 逻辑 | && || ! | 短路求值,仅接受 bool |
| 位运算 | & | ^ << >> | 整数逐位操作;! 也是按位取反 |
| 复合赋值 | += -= <<= 等 | 所有算术/位运算符均支持 |
| 引用/解引用 | & &mut * | 借用与解引用 |
| 其他 | . [] .. | 字段访问、索引、Range |
1 | |
⚠️ 注意:整数除法向零取整(
-7 / 2 == -3),与 Python 的向下取整(-7 // 2 == -4)不同,处理负数时要留意;需要向下取整用div_euclid/rem_euclid方法。Rust 没有++/--运算符,统一用x += 1–避免前置/后置语义的歧义。💡 提示:
!对bool是逻辑非,对整数是按位取反;&/|用于bool时不短路(两侧都会求值)。这些运算符都可被自定义类型重载–实现std::ops中对应的 trait(如Add、BitOr)即可。
运算符重载与 std::ops trait
Rust 的运算符并非语言内建魔法,而是 trait 的语法糖。当你写 a + b 时,编译器实际调用 a.add(b);写 a == b 时,调用 a.eq(b)。自定义类型只要实现对应 trait,就能"重载"运算符。这一设计让运算符的语义可扩展,同时保持类型系统的严格检查。
运算符与 trait 的对应关系:
| 运算符 | trait | 方法 | 示例 |
|---|---|---|---|
+ | std::ops::Add | add | a + b |
- | std::ops::Sub | sub | a - b |
* | std::ops::Mul | mul | a * b |
/ | std::ops::Div | div | a / b |
% | std::ops::Rem | rem | a % b |
-(一元) | std::ops::Neg | neg | -a |
!(按位) | std::ops::Not | not | !a |
& | std::ops::BitAnd | bitand | a & b |
| | std::ops::BitOr | bitor | a | b |
^ | std::ops::BitXor | bitxor | a ^ b |
<< | std::ops::Shl | shl | a << b |
>> | std::ops::Shr | shr | a >> b |
*(解引用) | std::ops::Deref | deref | *a |
[] | std::ops::Index | index | a[b] |
== != | std::cmp::PartialEq | eq ne | a == b |
< <= > >= | std::cmp::PartialOrd | lt le 等 | a < b |
下面为自定义结构体实现 Add trait,让 + 运算符作用于 Point 类型:
1 | |
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 | |
PartialOrd 与 Ord 的区别在于:Ord 要求全序关系(任意两元素可比),而 PartialOrd 允许存在不可比的元素对。f64 因 NaN 的存在只能实现 PartialOrd。如果自定义类型的比较逻辑天然全序,应该实现 Ord(通常通过 #[derive(Ord)] 同时派生 Eq + Ord)。
🔄 对比:Java 中所有对象都有
equals()和compareTo(),即使是 NaN 也"被迫"有比较结果。Rust 选择在类型系统层面区分"部分可比"与"全序可比",让排序、BTreeMap 等需要全序的场景在编译期就能拒绝f64这样的类型。
位运算细节
位运算作用于整数类型的二进制补码表示。理解补码是正确使用位运算的前提:
1 | |
移位运算的溢出行为需要特别留意:
1 | |
⚠️ 注意:
<<在 debug 模式下,若移位导致溢出(丢失非零位)会 panic;release 模式下回绕。移位量(右操作数)大于等于类型位宽时,debug 同样 panic。右移>>对无符号数补 0(逻辑右移),对有符号数补符号位(算术右移)–这与 C/C++ 的实现定义行为不同,Rust 的规则是确定的。
解引用、? 与赋值语句
三个需要单独说明的运算符:
解引用 *:取出引用指向的值。它依赖 Deref trait,标准库的 Box、Rc、String 等智能指针类型都实现了 Deref,使得 * 可以穿透多层引用。Deref 还支持隐式解引用转换(deref coercion)–&String 可以自动转为 &str。详见第 9 章:智能指针、迭代器与闭包。
1 | |
? 运算符:错误传播的语法糖。expr? 在 expr 为 Err 时提前返回错误,为 Ok 时解包值。它让错误处理代码大幅简化,详见第 7 章:错误处理与 Panic 恢复。
1 | |
赋值是语句:在 Rust 中,赋值 = 是语句而非表达式,求值为 ()。这与 C/C++ 截然不同:
1 | |
🔄 对比:C/C++ 中赋值是表达式(
a = b = c合法且常见),Rust 中赋值是语句返回()–let x = (y = 6);虽能编译但x的类型是(),几乎总是 bug。这一设计从语言层面杜绝了if (x = 1)这类"赋值误写为比较"的经典错误,是 Rust 追求安全的一个缩影。
Range(范围)语法
Range 是 Rust 中表示序列范围的重要语法,广泛用于循环迭代、切片操作和模式匹配。
基本语法
1 | |
切片中的 Range
1 | |
Range 类型
| 语法 | 类型 | 说明 |
|---|---|---|
start..end | Range<T> | 半开区间 |
start..=end | RangeInclusive<T> | 闭区间 |
start.. | RangeFrom<T> | 从 start 到无限 |
..end | RangeTo<T> | 从起点到 end |
..=end | RangeToInclusive<T> | 从起点到 end(包含) |
.. | RangeFull | 全范围 |
1 | |
Range 作为迭代器
Range 和 RangeInclusive 都实现了 Iterator trait,这意味着它们可以直接用于 for 循环和 collect,且所有迭代器适配器方法都可用:
1 | |
💡 提示:
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 | |
这是因为 RangeInclusive 在迭代前无法确定是否为空–它需要在第一次 next() 调用时检查 start > end,并设置 exhausted 标志。
消耗后不可复用:RangeInclusive 没有实现 Copy(因为有 exhausted 可变标志位),迭代消耗后不可复用:
1 | |
⚠️ 注意:如果需要在多处复用
RangeInclusive,用r.clone()显式克隆,或改用Range(半开区间)+ 手动处理边界。在大多数场景下,1..5比1..=5更方便,因为Range是Copy的。
rev() / step_by() 返回新迭代器
rev() 和 step_by() 是迭代器适配器方法,它们消费原迭代器并返回一个新的包装类型,而非原地修改。理解这一点有助于避免所有权相关的困惑:
1 | |
💡 提示:所有迭代器适配器(
map、filter、take、skip等)都遵循同样的模式:消费self,返回新类型。这是 Rust 零成本抽象的体现–适配器层层嵌套,但编译后会被内联优化为等价的循环,无运行时开销。
Range 在模式匹配中的应用
1 | |
⚠️ 注意:切片越界会 panic,用
get()安全访问:arr.get(1..5)返回Option。半开区间start >= end时 range 为空,循环体不执行。浮点数 range 只有闭区间..=可迭代,但不推荐(精度问题)。
Range 与切片语法(进阶)
🔬 进阶:Range 类型实现了
SliceIndextrait 的类型都可以用作[]索引参数–Range 类型正是如此。自定义类型只要实现SliceIndex,也能用于切片索引,这是 Rust 索引语法可扩展性的基础。
表达式与语句:Rust 的语法基石
Rust 是面向表达式(expression-oriented)的语言–这一设计继承自 ML/OCaml 的函数式血统,是理解 Rust 语法的第一把钥匙。与 C/Java/Go 等"语句为中心"的语言相比,Rust 的语法哲学可以浓缩为一句话:
几乎所有带花括号的构造都是表达式;分号的意思是"我不需要这个值"。
两者的严格定义:
- 语句(Statement):执行动作,不返回值(求值为
()),以分号结尾 - 表达式(Expression):求值为一个值,可嵌套在任何需要值的位置
语句只有三种
1 | |
几乎所有构造都能求值
1 | |
表达式求值时的 move / copy 语义
表达式不只是"计算出一个值"–在 Rust 中,表达式的求值涉及所有权转移。这是 Rust 表达式系统与函数式语言的关键差异:表达式求值时,值要么被复制(Copy 类型),要么被移动(非 Copy 类型)。
1 | |
这一规则延伸到所有表达式位置:运算符操作数、函数实参、数组元素、match 分支返回值等。理解这一点是避免"值在表达式中间被意外移动"这类编译错误的关键。
💡 提示:如果需要在表达式中"使用"非 Copy 值但不转移所有权,传引用(
&s或&mut s)。引用是 Copy 类型,传递引用不会移动原值。详见所有权、借用与生命周期。
块表达式与临时变量生命周期
代码块 {} 是表达式,其值为最后一个表达式的值。块内创建的临时变量在块结束时销毁,但块表达式的值会被"带出"块外:
1 | |
块表达式的这一特性让 Rust 的资源管理非常精确:你可以用块来限定临时变量的作用域,确保资源在正确时机释放,同时将计算结果以值的形式传出。
分支类型必须一致
if、match、loop 作为表达式时,所有分支必须返回相同的类型,否则编译错误。这是类型系统对表达式完整性的要求:
1 | |
⚠️ 注意:
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 | |
宏调用作为表达式
宏调用(如 vec![]、println!、format!)在语法上是表达式,求值为宏展开后表达式的值。宏在编译期展开为普通代码,展开后的表达式参与类型推断:
1 | |
宏与函数的区别在于:宏在编译期展开,可以接受任意数量的参数、生成任意代码;函数在运行期调用,参数类型固定。宏作为表达式时,其类型就是展开后代码的类型。
复杂表达式何时拆分
Rust 的方法链和表达式嵌套可以写出非常紧凑的代码,但过度紧凑会损害可读性。何时拆分是一个工程判断:
1 | |
💡 提示:Rust 社区倾向于"适度链式"–2-4 步的方法链通常比等价的循环更清晰,但超过 5 步或包含复杂闭包时应考虑拆分。
clippy的clippy::redundant_closure等检查可以帮助发现可简化的表达式。
与 C / Java / Go 的角色对比
| 构造 | C / Java / Go | Rust |
|---|---|---|
if | 语句(Java 需 ?: 三元) | 表达式,可直接赋值 |
switch / match | 语句,fall-through 易出错 | 表达式,穷尽检查 |
代码块 { } | 语句组 | 表达式,值为最后一行 |
| 循环 | 语句 | loop 可用 break 带出值 |
赋值 = | 表达式(a = b = c 合法) | 语句,求值为 () |
| 函数体 | 必须显式 return | 尾部表达式即返回值 |
分号规则与经典陷阱
分号是"表达式 -> 语句"的转换器。加上分号,值被丢弃,求值结果变为单元类型 ():
1 | |
函数返回值的经典陷阱:
1 | |
💡 提示:
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 | |
💡 提示:条件必须是
bool,Rust 不会像 C/C++ 那样将整数隐式转为布尔值。if 1 { }是编译错误。if作为表达式时,所有分支的类型必须一致,否则编译报错mismatched types。
while
while 是条件循环,当条件为 false 时退出。它适合不确定迭代次数的场景:
1 | |
💡 提示:优先用
for+ 迭代器替代while+ 索引–for不会越界且更符合 Rust 惯例。while适合条件不依赖集合迭代的场景(如轮询、状态机)。
while let
while let 是 while 与模式匹配的结合:每次循环前先尝试匹配,匹配成功则执行循环体,匹配失败则退出循环。它特别适合"持续从迭代器或 Option 取值直到取完"的场景:
1 | |
while let 与 if let 的区别在于:if let 只匹配一次,while let 持续匹配直到失败。while let 本质上是 loop { match ... { Some(x) => ..., None => break } } 的语法糖。
for 与所有权
for 循环是 Rust 中最常用的循环形式,它通过三种迭代方法与集合交互,选择哪种方法直接影响所有权语义:
1 | |
三种迭代方式对比:
| 方法 | 产出类型 | 借用形式 | 消费所有权 | 典型用途 |
|---|---|---|---|---|
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 | |
标签循环
嵌套循环中,标签(label)可以精确控制 break / continue 作用于哪一层。标签以 'label: 形式声明:
1 | |
🔄 对比:Rust 的标签语法
'label:类似 Go 的label:,但 Rust 标签只能与break/continue配合,不能像 Go 那样goto。标签名使用单引号前缀(与生命周期语法一致),这在视觉上与变量名区分开。
match 详解
match 是 Rust 最强大的控制流构造,支持丰富的模式匹配语法。完整详解见结构体、枚举与模式匹配,此处介绍核心模式:
1 | |
守卫(guard):在模式后附加 if 条件,对匹配的值做额外检查:
1 | |
绑定 @:将匹配到的值绑定到变量,同时测试范围:
1 | |
解构模式:对结构体、元组、枚举进行解构:
1 | |
穷尽性要求:match 必须覆盖所有可能的模式,否则编译错误。_ 通配符兜底,但过度使用 _ 会掩盖遗漏的分支:
1 | |
💡 提示:
match的穷尽性检查是 Rust 的一大安全特性–当你后续给枚举添加新变体时,所有match表达式会在编译期报错,提醒你处理新情况。尽量避免用_兜底,显式列出所有变体更安全。
if let / while let / let else 决策对比
Rust 提供了三种 match 的快捷语法,各有适用场景。选择正确的构造让代码更简洁:
只关心一种匹配情况时,if let 比完整 match 更简洁:
1 | |
三种快捷语法的决策对比:
| 构造 | 适用场景 | 是否穷尽 | 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 | |
💡 提示:
match是 Rust 最强大的控制流构造,支持解构、守卫、范围模式等。完整的模式匹配详解见结构体、枚举与模式匹配。
命名规范速查
基本风格
| 项 | 风格 | 示例 |
|---|---|---|
| 变量 / 函数 / 模块 | snake_case | user_name, read_file |
| 类型 / trait / 枚举 | CamelCase | UserProfile, Display |
| 常量 / 静态变量 | SCREAMING_SNAKE_CASE | MAX_RETRIES |
| 生命周期 | 短小写字母 | 'a, 'input |
| crate / 包名 | snake_case | serde_json |
前缀惯例
Rust 社区有一套广泛遵守的命名前缀惯例,让函数的返回类型和意图一目了然:
| 前缀 | 用途 | 返回类型 | 示例 |
|---|---|---|---|
is_ | 布尔状态查询 | bool | is_empty() |
has_ | 拥有性查询 | bool | has_error() |
can_ | 能力查询 | bool | can_retry() |
try_ | 可能失败的操作 | Result / Option | try_parse() |
1 | |
💡 提示:标准库中
is_前缀最为常见(is_empty、is_nan、is_some)。try_前缀通常表示返回Result或Option,但并非强制–如try_into返回Result,而try_get在某些集合中返回Option。关键是保持项目内一致性。
类型转换命名
类型转换方法的命名前缀遵循一套精确的约定,反映转换的开销和所有权语义:
| 前缀 | 语义 | 开销 | 所有权 | 示例 |
|---|---|---|---|---|
as_ | 廉价转换,返回借用 | 低 | 返回 &T | as_str() |
to_ | 昂贵转换,创建新对象 | 高 | 产出新 T | to_string() |
into_ | 消费转换 | - | 消费 self | into_vec() |
1 | |
🔄 对比:
as_vsto_的区别类似于&strvsString–as_str()返回借用(零拷贝),to_string()分配新内存。into_消费原对象,适合不再需要原值的场景。这一命名约定让调用者从方法名就能推断性能特征。
getter 命名
Rust 的 getter 方法不加 get_ 前缀,直接用字段名作为方法名。这与 Java/Python 的惯例不同:
1 | |
🔄 对比:Java 惯例是
getName()/getAge(),Python 是get_name()。Rust 省略get_前缀的理由是:方法名已经表达了"获取"的语义,加前缀是冗余。clippy的wrong_self_convention检查会对此发出警告。例外:当 getter 涉及计算或 IO 时,可以用get_前缀区分(如get_random_value)。
💡 提示:不符合命名规范时编译器会给出
non_snake_case等警告(不是错误);cargo clippy的检查更严格。统一的风格让阅读陌生 Rust 代码的成本显著降低。🔄 对比:Rust 是少有的在编译器层面强制命名风格的主流语言(Go 用 gofmt 管格式但不管命名,Python 的 PEP 8 只是社区约定)。
小结
- 运算符是 trait 的语法糖:
a + b调用Add::add,a == 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各有适用场景
下一篇将进入函数与输入输出,看看这些语法元素如何组织成可交互的程序。

