在 Rust 中,语句与表达式构成了代码的基本骨架。Rust 更偏向表达式风格:几乎一切都有值,代码块也能返回值; 但与此同时,分号会把表达式变为语句,从而影响返回值与类型推断。掌握这些基础,我们才能在后续内容中自如地组合控制流、设计函数返回、组织模式匹配与错误传播。
这节课我们围绕变量绑定与可变性、作用域与代码块、分支控制、循环与标签、模式匹配与简写、分号与返回值、控制流中的所有权交互等主题展开,形成系统的表达式直觉。

Rust 采用“默认不可变”的绑定策略,这是一个深思熟虑的设计决定。当我们声明一个变量时,除非显式标记为可变,否则它就是不可变的。这种策略带来了多重好处: 首先,它让代码更容易推理。当我们看到一个变量绑定后,就能确信它的值不会在后续代码中被意外修改。这极大地减少了心智负担,特别是在阅读复杂函数或调试问题时。 其次,不可变性是并发安全的基础(我们会在并发部分详细介绍)。不可变数据天然就是线程安全的,因为没有竞态条件的风险。
通过 let 关键字我们可以创建变量绑定。Rust 会根据使用情况自动推断类型,但我们也可以显式标注:
fn main() {
let x = 10; // 不可变绑定
let mut y = 3; // 可变绑定
println!("x = {x}, y = {y}");
y += 4;
println!("after: x = {x}, y = {y}");
}x = 10, y = 3
after: x = 10, y = 7遮蔽(shadowing)是 Rust 中一个强大而优雅的特性,它允许我们使用同一个名字重新声明变量。这种机制既保持了"默认不可变"设计的安全性,又为我们提供了灵活的数据转换能力。 遮蔽的核心优势在于:它创建了全新的变量绑定,而不是修改原有变量的值。这意味着每次遮蔽都可以改变类型,同时保持变量名的一致性,让代码更加清晰易读。
与可变变量不同,遮蔽允许我们在类型转换的同时保持不可变性。比如,我们可能需要将字符串解析为数字,然后对数字进行计算。使用遮蔽,整个过程中的每个步骤都是不可变的,但我们可以逐步"演进"数据的形态。 另一个重要特点是作用域行为:新的遮蔽变量会在当前作用域内生效,当离开作用域时,之前的绑定会重新可见。
fn main() {
let v = "42"; // &str
let v: i32 = v.parse().unwrap(); // 遮蔽为 i32
let v = v * 2; // 再次遮蔽为新的数值
println!("v = {v}");
}v = 84在 Rust 中,大括号 {} 包裹的代码块具有双重身份:它既是作用域的边界,也是一个表达式。这种设计让我们能够以更加优雅和函数式的方式组织代码。
作为作用域边界:代码块为变量绑定创建了独立的生命周期。在块内声明的变量只在该块内可见,一旦离开块的范围,这些变量就会被销毁。这种机制帮助我们:
作为表达式:代码块可以产生值,这个值由块中最后一个表达式决定。关键在于理解"表达式"与"语句"的区别:
5 + 3、x * 2let x = 5;如果代码块的最后一行是一个不带分号的表达式,那么这个表达式的值就成为整个块的值。如果最后一行带有分号,或者是一个语句,那么块的值就是单元类型 ()。
代码块作为表达式的特性在实际编程中非常有用,特别是在需要进行复杂计算或条件逻辑时:
fn main() {
let outer = 10;
let result = {
let inner = outer * 2;
inner + 5 // 注意:无分号,块的返回值
};
println!("result = {result}");
}result = 25如果在末尾加上分号,块就不再返回该表达式的值,而是返回 ()(单元类型)。这个细节会直接影响函数返回与类型推断。

Rust 的 if 语句本质上是一个表达式,这意味着它不仅能控制程序流程,还能产生并返回值。这种设计哲学源于函数式编程的理念,让我们能够以更加简洁和直观的方式编写代码。
传统的命令式语言通常需要我们先声明一个变量,然后在不同的条件分支中为这个变量赋值。而在 Rust 中,我们可以直接让 if 表达式的结果赋值给变量,避免了引入不必要的临时变量和多次赋值操作。
条件必须明确为布尔类型:Rust 要求 if 的条件部分必须是 bool 类型,不接受任何形式的隐式类型转换。这与一些语言中"非零即真"的约定不同。例如,你不能写 if 5 { ... },必须写成 if x != 0 { ... } 这样的明确比较。这种严格性虽然增加了一些代码量,但大大减少了因类型混淆而产生的逻辑错误。
表达式特性的实际应用:当 if 作为表达式使用时,每个分支都必须返回相同类型的值(或者能够被编译器统一的类型)。这确保了无论程序执行哪个分支,最终的结果类型都是确定和一致的。如果某个分支没有显式返回值,编译器会将其视为返回单元类型 ()。
fn main() {
let score = 86;
let grade = if score >= 90 {
'A'
} else if score >= 80 {
'B'
} else {
'C'
};
println!("grade = {grade}");
}grade = B分支的返回类型必须一致或能统一,否则编译器无法推断。表达式风格要求每个分支的值类型对齐,以保证求值确定性。
Rust 的 match 表达式是一个功能强大且严谨的控制流工具,它通过模式匹配机制能够精确识别和区分值的不同形态、范围以及结构特征。
与其他语言中相对宽松的 switch 语句不同,Rust 的 match 强制实行“穷尽匹配”原则,这意味着我们必须为所有可能出现的值情况提供对应的处理分支。
当我们为某个类型的所有可能取值都提供了匹配分支时,编译器就能在编译阶段确保程序的完整性和安全性,保证运行时不会遇到未处理的情况。 相反,如果遗漏了某些可能的值或模式,编译器会立即报错,强制我们重新审视代码逻辑,考虑那些可能被忽略的边界情况和异常状态。
fn describe(n: i32) -> &'static str {
match n {
0 => "zero",
1 | 2 => "one or two",
3..=9 => "three to nine",
_ => "others",
}
}
fn main() {
for n in [0, 2, 5, 42] { println!("{n}: {}", describe(n)); }
}0: zero
2: one or two
5: three to nine
42: others在 match 分支体中,同样遵循表达式规则:最后一个无分号表达式作为分支值,所有分支的值类型必须一致。
Rust 提供了三种基本的循环构造,每种都有其特定的使用场景和设计理念。

loop 关键字创建一个无条件的无限循环,它会持续执行直到程序主动使用 break 语句终止。这种循环的独特之处在于它可以通过 break 语句返回一个值,让循环本身成为一个表达式。
这种设计使得我们可以在循环内部计算出某个结果,然后将这个结果作为整个循环表达式的值传递给外部变量。
while 循环则是条件驱动的循环结构,它会在每次迭代开始前检查给定的布尔表达式。只
要条件为真,循环就会继续执行;一旦条件变为假,循环立即终止。这种循环适合那些需要在某个状态满足时持续执行的场景。
for 循环是 Rust 中最常用也是最安全的迭代方式,它专门用于遍历实现了迭代器 trait 的数据结构。
相比于传统的索引访问方式,for 循环天然避免了数组越界等常见错误,同时提供了更清晰和表达力更强的代码。
它不仅可以遍历数组和切片,还能处理各种集合类型以及任何实现了 IntoIterator trait 的自定义类型。
fn main() {
// loop + break 返回值
let mut cnt = 0;
let value = loop {
cnt += 1;
if cnt == 3 { break cnt * 10; }
};
println!("value from loop = {value}");
// while 条件循环
let mut n = 5;
while n > 0 {
print!("{n} ");
n -= 1;
}
println!("go!");
// for 遍历
for ch in ["R", "u", "s", "t"] { print!("{ch}"); }
println!();
}value from loop = 30
5 4 3 2 1 go!
Rust当我们遇到多层循环嵌套时,单纯使用 break 或 continue 只能作用于最近的一层循环。如果想要直接跳出外层循环或者控制特定层级的循环流程,Rust 提供了“循环标签”机制。
我们可以在循环前加上一个以单引号 ' 开头的标签(比如 'outer:),然后在循环体内通过 break 'outer 跳出对应的循环,或者用 continue 'outer 直接进入外层循环的下一轮。
这种方式让我们在复杂的嵌套结构中也能清晰、精准地控制流程,避免了冗长的标志变量和混乱的逻辑判断。
举个例子:假设我们有两层循环,内层循环遇到某个条件时希望直接跳出外层循环,这时就可以用标签来实现。如果不用标签,break 只能终止内层循环,外层还会继续执行;而有了标签,我们可以一行代码直接跳出所有需要的循环层级。
fn main() {
'outer: for i in 1..=3 {
for j in 1..=3 {
if i * j == 4 {
println!("i={i}, j={j}, break outer");
break 'outer;
}
}
}
println!("done");
}i=2, j=2, break outer
doneif let 与 let-else有时候我们只想处理某种特定的匹配情况,比如只在解析成功时才继续后续逻辑,这时用 if let 能让代码变得更简洁,省去了完整 match 的冗余结构。if let 让我们可以直接针对关心的分支写处理逻辑,忽略其他情况。
到了 Rust 1.65,let-else 语法进一步提升了表达力:我们可以在变量绑定时直接写出匹配条件,如果不满足就立刻返回或提前退出,这样一来,遇到不符合预期的情况时,代码会自动帮我们“早退”,让主流程更聚焦于成功路径,错误分支也更清晰。
fn parse_even(input: &str) -> Result<i32, String> {
if let Ok(n) = input.parse::<i32>() {
if n % 2 == 0 { Ok(n) } else { Err("not even".into()) }
} else {
Err("not a number".into())
}
}
fn head_as_number(words: &[&str]) -> Result<i32, String> {
let Some(first) = words.first() else {
return Err("empty slice".into());
};
first.parse().map_err(|_| "parse error".into())
}
fn main() {
println!("{:?}", parse_even("10"));
println!("{:?}", parse_even("7"));
println!("{:?}", head_as_number(&["12", "34"]));
println!("{:?}", head_as_number(&[]));
}Ok(10)
Err("not even")
Ok(12)
Err("empty slice")在 Rust 里,分号的作用是把一个表达式变成一条语句,这样它的值就不会被后续代码使用了。
我们在写函数时,通常会让函数体的最后一行是一个没有分号的表达式,这样这个表达式的值就会作为整个函数的返回值。
如果最后一行加了分号,返回的就不是我们期望的值,而是空元组 (),这往往会导致类型不匹配的编译错误。
这种设计和 Rust 把代码块当作表达式的理念是一致的,也就是说,代码块的最后一个没有分号的表达式决定了整个块的值。
fn add(a: i32, b: i32) -> i32 {
a + b // 无分号:作为返回值
}
fn add_wrong(a: i32, b: i32) -> i32 {
a + b; // 有分号:返回 (),与签名不匹配,编译报错
0
}
fn main() {
println!("{}", add(2, 3));
}5return、break、continue在 Rust 的表达式风格代码中,我们依然可以通过 return 语句实现函数的提前返回,这样一旦遇到特定条件,函数就会立刻结束并返回指定的值。
在循环体内部,如果我们想要直接跳出整个循环,可以使用 break,而且在 Rust 里,break 还允许我们带上一个值作为循环表达式的结果。
至于 continue,它的作用是立即结束本轮循环,直接进入下一次迭代。合理地运用这些控制流语句,能够让我们的意图表达得更加清晰,代码结构也会更加直观易懂。
fn find_first_even(nums: &[i32]) -> Option<i32> {
for &n in nums {
if n % 2 == 0 { return Some(n); }
}
None
}
fn main() {
println!("{:?}", find_first_even(&[1, 3, 5]));
println!("{:?}", find_first_even(&[1, 4, 6]));
}None
Some(4)在 Rust 的表达式语境下,变量的所有权会随着分支或代码块的不同路径发生转移或者被借用。编译器会严格追踪每个值的所有权流向,确保我们不会在值被移动之后还继续使用它。 如果我们在 if 或 match 的某个分支里把一个变量的所有权转移出去了,那么在后续代码中,这个变量就不能再被访问了。 假如我们希望在分支之后还能继续用到原来的值,就需要提前考虑,是不是应该只借用它,或者在需要的时候进行克隆。这样既能保证代码的安全性,也能让所有权的流转更加清晰可控。
fn choose_str(cond: bool) -> String {
let a = String::from("A");
let b = String::from("B");
let chosen = if cond { a } else { b }; // 移动其中一个所有权
chosen
}
fn main() {
println!("{}", choose_str(true));
}在上例中,a 或 b 会被移动到 chosen,不可再用。若我们只想读取它们而不移动,可借用:
fn choose_ref<'a>(cond: bool, a: &'a str, b: &'a str) -> &'a str {
if cond { a } else { b } // 借用,避免移动
}
fn main() {
let s1 = String::from("hello");
let s2 = String::from("world");
let r = choose_ref(true, &s1, &s2);
println!("{} {} -> {}", s1, s2, r);
}hello world -> hello在 Rust 里,if 或 match 这样的分支表达式,要求所有分支最终都要产生同一种类型的值。比如 if 的 true 分支和 false 分支,或者 match 的每个分支,类型都要一致。 否则,编译器就无法推断出整个表达式的类型。如果我们遇到分支类型不一致的情况,可以通过类型转换(比如 as 关键字),或者把不同类型包裹进同一个枚举、特征对象等方式, 让它们在类型上统一起来。只有这样,编译器才能顺利通过类型检查,整个表达式才能作为一个值被使用。
fn main() {
let flag = true;
let value = if flag { 10 } else { 20 }; // 两边同为 i32,OK
println!("{value}");
}当两个分支类型不同,可以显式统一:
fn main() {
let flag = true;
let value: Box<dyn std::fmt::Display> = if flag {
Box::new(10)
} else {
Box::new("ten")
};
println!("{}", value);
}match 的绑定与守卫在 Rust 的模式匹配中,我们不仅可以通过模式直接提取出值,还能在匹配的同时把某些部分绑定到变量上,方便后续使用。 如果我们希望对某个分支增加额外的判断条件,还可以在模式后面加上 if 语句,这就是所谓的“守卫”。 这样,只有当模式匹配且守卫条件成立时,这个分支才会被选中。通过绑定和守卫的结合,我们可以灵活地对数据进行分类和处理,让 match 表达式既简洁又强大。
fn classify(n: i32) -> &'static str {
match n {
x if x < 0 => "neg",
0 => "zero",
x if x % 2 == 0 => "pos-even",
_ => "pos-odd",
}
}
fn main() {
for n in [-2, -1, 0, 3, 4] { println!("{n}: {}", classify(n)); }
}-2: neg
-1: neg
0: zero
3: pos-odd
4: pos-even当我们用模式匹配结构体或枚举时,可以直接在模式中拆解出各个字段的值。这样不仅能方便地访问内部数据,还能灵活选择是获取所有权、可变借用还是只读借用。 例如,我们可以只借用某个字段,避免整个对象被移动,从而继续在后续代码中使用原始变量。这种解构方式让我们在处理复杂数据类型时既高效又安全。
#[derive(Debug)]
struct User { id: u32, name: String, active: bool }
fn name_if_active(u: &User) -> Option<&str> {
let User { name, active, .. } = u; // 借用字段
if *active { Some(name.as_str()) } else { None }
}
fn main() {
let u = User { id: 1, name: "Li".into(), active: true };
println!("{:?}", name_if_active(&u));
}let-模式与解构绑定在 Rust 里,let 语句不仅仅是简单地给变量赋值,更是一种模式匹配和绑定的工具。我们可以利用 let 直接把元组、结构体或者枚举的内部数据拆解出来,并分别绑定到新的变量上,这样后续就能方便地单独使用每一部分的数据。
fn main() {
let pair = (3, "hi");
let (a, b) = pair;
println!("a={a}, b={b}");
}?在 Rust 里,? 运算符让我们能够在遇到错误时自动把错误返回给调用者,而不是手动去判断和处理每一步的错误。
这样写出来的代码既简洁又符合 Rust 一贯的表达式风格。只要某个操作返回 Result,我们在后面加上 ?,如果成功就继续往下执行,如果出错就会立刻把错误返回出去,整个过程非常自然地融入到函数的控制流中。
use std::fs;
use std::io;
fn load_conf(path: &str) -> Result<String, io::Error> {
let content = fs::read_to_string(path)?; // 失败则提前返回 Err
Ok(content)
}
fn main() {
match load_conf("missing.conf") {
Ok(s) => println!("{}", s),
Err(e) => eprintln!("error: {e}"),
}
}9. if表达式赋值练习
使用if表达式直接赋值,而不是先定义可变变量再在分支里赋值。
fn main() {
let score = 85;
// 方式1:使用if表达式直接赋值(推荐)
let grade = if score >= 90 {
"A"
} else if score >= 80 {
"B"
} else if score >= 70 {
"C"
} else {
"D"
};
println!("分数: {}, 等级: {}", score, grade);
// 方式2:使用match表达式(另一种选择)
let grade2 = match score {
90..=100 => "A",
80..=89 => "B",
70..=79 => "C",
_ => "D",
};
println!("分数: {}, 等级: {}", score, grade2);
// 演示:if表达式可以返回不同类型的值(但需要相同类型)
let result = if score > 60 {
"及格" // 字符串
} else {
"不及格" // 字符串(必须相同类型)
};
println!("结果: {}", result);
}分数: 85, 等级: B
分数: 85, 等级: B
结果: 及格说明:
10. match表达式和守卫条件练习
使用match表达式匹配三态枚举,要求穷尽匹配,并加入守卫条件。
#[derive(Debug)]
enum Status {
Success,
Warning(i32), // 带数据的变体
Error(String),
}
fn status_to_string(status: Status) -> String {
match status {
Status::Success => "成功".to_string(),
Status::Warning(code) if code > 100 => {
format!("严重警告: 错误码 {}", code)
},
Status::Warning(code) => {
format!("警告: 错误码 {}", code)
},
Status::Error(msg) if msg.len() > 10 => {
format!("严重错误: {}", msg)
},
Status::Error(msg) => {
format!("错误: {}", msg)
},
}
}
fn main() {
let statuses = vec![
Status::Success,
Status::Warning(50),
Status::Warning(150),
Status::Error("连接失败".to_string()),
Status::Error("数据库连接超时,请检查网络".to_string()),
];
for status in statuses {
println!("{:?} -> {}", status, status_to_string(status));
}
}Success -> 成功
Warning(50) -> 警告: 错误码 50
Warning(150) -> 严重警告: 错误码 150
Error("连接失败") -> 错误: 连接失败
Error("数据库连接超时,请检查网络") -> 严重错误: 数据库连接超时,请检查网络说明:
11. loop + break返回值练习
使用loop循环和break返回值,统计直到遇到第一个负数之前的非负数个数。
fn count_until_negative(numbers: &[i32]) -> usize {
let mut count = 0;
let result = loop {
if count >= numbers.len() {
break count; // 没有负数,返回总数
}
if numbers[count] < 0 {
break count; // 遇到负数,返回当前计数
}
count += 1;
};
result
}
fn main() {
let test_cases = vec![
vec![1, 2, 3, -1, 4, 5],
vec![10, 20, 30, 40],
vec![-5, 1, 2, 3],
vec![0, 1, 2, 3, 4, 5, -10],
];
for numbers in test_cases {
let count = count_until_negative(&numbers);
println!("数组: {:?}, 非负数个数: {}", numbers, count);
}
}数组: [1, 2, 3, -1, 4, 5], 非负数个数: 3
数组: [10, 20, 30, 40], 非负数个数: 4
数组: [-5, 1, 2, 3], 非负数个数: 0
数组: [0, 1, 2, 3, 4, 5, -10], 非负数个数: 6说明:
break value返回值12. let-else模式练习
使用let-else在函数开头对输入进行快速校验,未满足条件则早退。
fn process_number(input: Option<i32>) -> Result<String, String> {
// 使用let-else进行快速校验
let Some(value) = input else {
return Err("输入为空".to_string());
};
// 继续处理,value已经解构出来
if value < 0 {
return Err("数值不能为负数".to_string());
}
Ok(format!("处理成功: {}", value * 2))
}
fn process_user(user: Option<&str>) -> String {
// let-else模式:如果匹配失败,执行else分支(早退)
let Some(name) = user else {
return "用户不存在".to_string();
};
// name已经解构出来,可以直接使用
format!("欢迎, {}!", name)
}
fn main() {
// 测试process_number
let results = vec![
process_number(Some(10)),
process_number(Some(-5)),
process_number(None),
];
for result in results {
match result {
Ok(msg) => println!("成功: {}", msg),
Err(err) => println!("错误: {}", err),
}
}
// 测试process_user
println!("\n用户处理:");
println!("{}", process_user(Some("张三")));
println!("{}", process_user(None));
}成功: 处理成功: 20
错误: 数值不能为负数
错误: 输入为空
用户处理:
欢迎, 张三!
用户不存在说明: