Rust 之九 语句和表达式、控制流、作用域、生命周期
概述
Rust 的基本语法对于从事底层 C/C++ 开发的人来说多少有些难以理解,虽然官方有详细的文档来介绍,不过内容是相当的多,看起来也费劲。本文通过将每个知识点简化为 一个 DEMO + 每种特性各用一句话描述的形式来简化学习过程,提高学习效率。
所有 DEMO 可以从 https://gitee.com/itexp 获取
基本概念
在之前的博文 Rust 之六 运算符、标量、元组、数组、字符串、结构体、枚举 我们学习了 Rust 的数据类型,其中很多地方及示例有用的变量、函数等这些 Rust 的代码的基本组成部分,接下来我们就先学习一下 Rust 代码的基本组成单元。
程序项
程序项(Item)是 Rust 源代码的组织单元。程序项是 Rust 程序中的顶层结构,构成了 Rust 程序的基本骨架。程序项在编译时就完全确定下来了,通常在执行期间保持结构稳定,并可以驻留在只读内存中。有以下几类程序项:
- 模块
- 外部 crate 声明
extern crate - use 声明
- 函数定义
- 类型定义
- 结构体定义
- 枚举定义
- 联合体定义
- 常量项
- 静态项
- trait 定义
- 实现
- 外部块
extern blocks
语句和表达式
Rust 是一门基于表达式(expression-based)的语言。在 Rust 中,语句(Statements)和表达式(Expressions)是两种基本的代码构建块,它们在 Rust 的控制流和语法中有重要的作用。
- 语句是执行一些操作但不返回值的指令。 变量的定义是语言;函数定义也是语句
// 变量绑定(声明语句) let x = 5; // 声明语句 // 表达式语句(表达式 + 分号) println!("Hello!"); // 表达式 `println!` 后加分号变为语句 - 表达式计算并产生一个值。 单独的一个字面值是一个表达式;函数调用是一个表达式;宏调用是一个表达式;用大括号创建的一个新的块作用域也是一个表达式
如果代码块的最后一行不加分号,它是一个表达式,返回该行的值。如果加了分号,则变成语句,返回// 字面量表达式 42 // 返回整数 42 // 算术表达式 3 + 2 // 返回 5 // 函数调用(是表达式) let x = add(3, 4) // 返回 7 // {} 代码块是一个表达式,但是 let y = {}; 是语句 let y = { let a = 1; a + 1 // 代码块的值是 2。如果这里加了分号就变成语句了! };()(单元元组)。let a = { 5 }; // a = 5 let b = { 5; }; // b = () - 表达式可以是语句的一部分,一个典型的示例(后续我们再详细分析):
let result = if x > 10 { "Greater than 10" } else { "Less than or equal to 10" };
控制流
控制流在编程语言中扮演着至关重要的角色,它决定了程序执行的顺序和逻辑结构。控制流用于控制程序的执行路径,指示程序在不同情况下如何跳转、循环或执行不同的代码段。通过控制流,程序可以根据不同的条件、循环或异常处理来动态调整行为。
if
if 表达式允许根据条件执行不同的代码分支。你提供一个条件并表示 “如果条件满足,运行这段代码;如果条件不满足,不运行这段代码或者执行另一段代码。
fn main() {
/* =========================== 基本 if 控制块 ============================ */
let number = 6;
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");
}
/* ======================= 在 let 语句中使用 if ========================== */
let condition = true;
let number = if condition { 5 } else { 6 }; // if 分支中的值必须与 else 分支中的值相同
println!("The value of number is: {}", number);
}
-
if表达式中与条件关联的代码块有时被叫做 arms,并且可以只有一个if xx { }而将else { }可以省略,也可以将else if表达式与 if 和 else 组合来实现多重条件 -
必须总是显式地使用
bool类型作为 if 的条件。 不像 Ruby 或 JavaScript 这样的语言,Rust 并不会尝试自动地将非布尔值转换为布尔值。 -
因为
if是一个表达式,我们可以在 let 语句的右侧使用它。但是,if 的每个分支的可能的返回值都必须是相同类型!
loop
loop 关键字告诉 Rust 一遍又一遍地执行一段代码直到你明确要求停止。
fn main() {
/* ========================== 普通 loop 控制流 =========================== */
let mut counter = 0;
loop {
counter += 1;
if counter == 5 {
continue;
}
println!("Counter: {counter}");
if counter == 10 {
break;
}
}
/* ======================== 在 let 语句中使用 loop ======================= */
let mut counter = 0;
let result = loop {
counter += 1;
if counter == 10 {
break counter * 2;
}
};
println!("The result is {result}");
/* ============================ 嵌套 loop =============================== */
let mut count = 0;
'counting_up: loop {
println!("count = {count}");
let mut remaining = 10;
loop {
println!("remaining = {remaining}");
if remaining == 9 {
break;
}
if count == 2 {
break 'counting_up;
}
remaining -= 1;
}
count += 1;
}
println!("End count = {count}");
}
-
使用
continue关键字可以忽略continue之后的代码,再次从头开始下一次循环 -
使用
break关键字可以跳出循环。当存在嵌套循环时,break和continue仅对所在的循环起作用,可以选择在一个循环上指定一个 循环标签(loop label),然后将标签与break或continue一起使用 -
因为
loop是一个表达式,我们可以在let语句的右侧使用它。但是,此时必须使用break 值;返回一个值
while
while 循环实现当条件为 true,执行循环;当条件不再为 true,则停止循环。实际上就是对使用 loop、if、else 和 break 时所必须的嵌套的一个简化。
fn main() {
/* =========================== 普通 while 循环 =========================== */
let mut number = 3;
while number != 0 {
println!("{number} ");
number -= 1;
}
/* ============================= 提前终止循环 ============================ */
let mut number = 6;
while number != 0 {
println!("{number} ");
number -= 1;
if number == 4 {
continue;
}
if number == 2 {
break;
}
}
/* ========================== let 中使用 while ========================== */
let result = while false {}; // result 的类型为 ()
// 以下代码会编译错误
// let x = while true {
// break 42; // 错误:`while` 循环不能返回值
// };
}
-
与
if类似,条件表达式必须返回bool类型,不允许隐式转换 -
通过条件自动终止,或使用
break提前退出,也可以使用continue跳过当前迭代 -
与
loop不同,while不能直接作为表达式返回值,其类型始终为()(单元类型)。
for
for 循环实现在一个指定的范围进行循环,非常适用于对一个集合(如数组、范围、迭代器)的每个元素遍历并执行一些代码的情况。for 循环的安全性和简洁性使得它成为 Rust 中使用最多的循环结构。
fn main() {
/* ============================== 遍历数组 ============================== */
let a = [10, 20, 30, 40, 50];
for element in a {
println!("the value is: {element}");
}
/* ============================== 遍历范围 ============================== */
for i in 0..3 { // 0 ≤ i < 3
println!("i = {}", i);
}
/* ============================== 惰性求值 ============================== */
let nums = (1..).filter(|x| x % 2 == 0); // 无限生成偶数,但仅按需计算
for n in nums.take(3) { // 取前 3 个
println!("偶数: {}", n); // 输出 2, 4, 6
}
/* ============================== 元组解构 ============================== */
let pairs = [(1, "一"), (2, "二")];
for (num, word) in pairs { // 解构元组
println!("{}: {}", num, word);
}
/* ============================== 嵌套循环 ============================== */
'outer: for x in 0..5 {
for y in 0..5 {
if x + y > 3 {
break 'outer; // 直接跳出外层循环
}
}
}
}
match
match 允许我们将一个值与一系列的模式相比较,并根据相匹配的模式执行相应代码。它类似于其他语言的 switch-case,但更强大、更灵活,并且强制穷尽所有可能情况(exhaustive checking),避免遗漏。
match value {
pattern1 => expression1,
pattern2 => expression2,
_ => default_expression, // 默认分支(类似 `default`)
}
其中,value 是被匹配的值,可以是任意标量类型的变量,也可以结构体变量、枚举变量等;pattern 是匹配模式,可以是字面量、变量、通配符、.. 范围、结构体、枚举变体等。并且可以用 _ 表示默认情况(类似 switch 的 default);expression 是匹配成功时执行的代码块(可以是表达式或语句块)。
-
模式解构(Destructuring):解构结构体、枚举、元组、数组等复杂类型
struct Point { x: i32, y: i32 } let point = Point { x: 1, y: 2 }; match point { Point { x, y } => println!("x={}, y={}", x, y), } -
绑定变量(Binding Values):在匹配模式的同时绑定变量
let value = 42; match value { x @ 0..=50 => println!("Small: {}", x), // 将匹配的值绑定到 x x @ 51..=100 => println!("Large: {}", x), _ => (), } -
匹配守卫(Match Guards):在模式后添加 if 条件,进一步过滤匹配
let num = Some(42); match num { Some(x) if x > 10 => println!("Large number: {}", x), Some(x) => println!("Small number: {}", x), None => (), } -
match 本身是一个表达式,我们可以在
let语句的右侧使用它,但所有分支的返回值类型必须一致let value = 3; let description = match value { 1 => "one", 2 => "two", _ => "many", // 所有分支返回 &str };
if let
在 Rust 中,if let 是一个结合了模式匹配和条件判断的语法糖,它主要用于简化某些需要处理特定模式的 match 表达式。if let 用于在 只关心一种匹配模式,而需要忽略其他所有可能情况时,替代冗长的 match 写法。
if let Pattern = Expression {
// 当 Expression 匹配 Pattern 时执行的代码
} else {
// 可选的 else 分支(处理其他情况)
}
其中,Pattern 是要匹配的模式(如 Some(x)、Ok(value) 等);Expression 是需要匹配的表达式(如 Some(5)、Ok(42))。else 部分可以省略。
let some_value = Some(3);
match some_value {// 使用 match
Some(3) => println!("Three"),
_ => (),
}
if let Some(3) = some_value {// 使用 if let
println!("Three");
}
作用域
作用域(Scope) 是一个程序项(item)在程序中的有效范围。作用域(Scope) 决定了变量、函数、模块等实体的可见性和生命周期,确保内存安全。
块级作用域
块级作用域即由 {} 定义的代码块,并且可以嵌套多层。这包括单独使用的 {},以及 {} 结合 Module 定义、函数定义、结构体定义、枚举定义等等的代码块。
// 顶层作用域:模块
mod User {
// 模块内部作用域
struct UserInfo {
name: String,
age: u32,
}
};
fn main() {
// main 函数中:局部作用域
greet();
println!("Max users allowed: {}", MAX_USERS);
// 使用 {} 创建一个嵌套的作用域
{ // s 在这里无效,它尚未声明
let s = "hello"; // 从此处起,s 是有效的
// 使用 s
} // 此作用域已结束,s 不再有效
let user = User {
name: String::from("Alice"),
age: 30,
};
println!("User: {}, Age: {}", user.name, user.age);
}
-
内层作用域可以访问外层变量,但外层不能访问内层变量
-
变量在作用域结束时将自动被释放资源
全局作用域
全局作用域指的是源文件中未嵌套在任何其他结构(如函数、模块或结构体)中的代码。它通常包含模块声明、常量、全局变量、外部库的引入以及函数定义等。
// main.rs
// 全局作用域:引入外部库
use std::collections::HashMap;
// 全局作用域:声明常量
const MAX_USERS: u32 = 1000;
// 全局作用域:函数定义,函数本身在全局作用域,因此在当前文件任何地方都可以使用
fn greet() {
println!("Hello, World!");
}
源文件本身就是一个独立的作用域,因此,不同源文件之间是不能互相使用对方的资源的,除非显示将对方的资源引入到自己源文件内部。
引用的作用域
在 Rust 中,引用的作用域由借用检查器(Borrow Checker)根据非词法生命周期(Non-Lexical Lifetime, NLL)规则确定,其核心是:引用的作用域从声明处开始,到最后一次使用为止(而不是代码块的结尾)。
生命周期
生命周期(Lifetime)是一种静态检查机制,用来描述引用在程序中的有效期。它确保引用在使用时不会超过它所指向的数据的有效时间,从而避免 悬垂引用(Dangling Reference) 等内存安全问题。
-
在 Rust 中,引用(
&T或&mut T)会借用数据的所有权,而生命周期就是用来描述这个借用的有效期的 -
作用域决定了一个变量的创建和销毁的时间,而生命周期决定了引用的有效期。
生命周期标注
Rust 的生命周期是由编译器通过 生命周期标注(lifetime annotations) 来追踪的,生命周期标注是 Rust 提供的一种语法,用来指明一个引用的生命周期是如何与其他变量的生命周期相关联的,它并不改变任何引用的生命周期的长短。
fn longest<'a>(s1: &'a str, s2: &'a str) -> &'a str {
if s1.len() > s2.len() {
s1
} else {
s2
}
}
fn main() {
let s1 = String::from("Hello");
let s2 = String::from("World");
let result = longest(&s1, &s2);
println!("The longest string is {}", result);
}
生命周期标注的格式为 在类型前面添加 '至少一个任意字符,如 'data、'ctx,但通常使用短名称 'a、'b 等 。在许多情况下,编译器可以自动推导生命周期,无需手动标注。但是,当编译器无法通过省略规则推断生命周期时,就需要我们手动来进行标注了。
- 生命周期省略规则(lifetime elision rules)
- 每个输入引用都有一个独立的生命周期参数。例如
fn foo<'a, 'b>(x: &'a i32, y: &'b i32)可以省略'a和'b的标注 - 如果只有一个输入引用,它的生命周期会被赋予所有输出引用。例如
fn foo<'a>(x: &'a i32) -> &'a i32)可以省略'a。 - 如果是方法(
&self或&mut self),输出的生命周期默认与self相同。例如fn method(&self) -> &str无须标注。
- 每个输入引用都有一个独立的生命周期参数。例如
- 显式标注
- 包含引用的结构体、枚举
- 函数返回引用且依赖多个输入参数
生命周期标注是一种编译期的静态分析工具,它告诉编译器如何有效的区分相关成员的有效期及之间的存活关系,它并不会对编译后的程序有任何影响,也不会影响程序运行时性能
具有相同的生命周期标注的成员共享最短生命周期。 编译器会取它们的生命周期交集(即最短的存活时间),并确保返回值或其他依赖项的生命周期不超过该范围。编译器会取 x 和 y 中生命周期较短的一个,作为返回值的生命周期。
fn longest<'a>(s1: &'a str, s2: &'a str) -> &'a str {
if s1.len() > s2.len() { s1 } else { s2 }
}
具有不同生命周期标注的成员之间的生命周期互相独立,互不影响。 若返回值需要同时依赖多个不同生命周期的参数,需通过生命周期子类型('a: 'b)或泛型约束明确关系。'b: 'a 表示 'b 至少和 'a 一样长。
fn longest_of_two<'a, 'b: 'a>(s1: &'a str, s2: &'b str) -> &'a str {
if s1.len() > s2.len() { s1 } else { s2 }
}
借用检查器
Rust 编译器有一个借用检查器(borrow checker),它比较作用域来确保所有的借用都是有效的。在编译时,Rust 比较这两个生命周期的大小,并发现 r 拥有生命周期 'a,不过它引用了一个拥有生命周期 'b 的对象。程序被拒绝编译,因为生命周期 'b 比生命周期 'a 要小。
fn main() {
let r; // ---------+-- 'a
// |
{ // |
let x = 5; // -+-- 'b |
r = &x; // | |
} // -+ |
// |
println!("r: {r}"); // |
} // ---------+
静态生命周期
静态生命周期('static) 是一种特殊的生命周期,表示引用所指向的数据在整个程序运行期间始终有效。如果引用字段指向静态数据(全局常量),可以使用 'static 生命周期,无需额外标注。
- 字符串字面量
let s: &'static str = "Hello, world!"; // 字符串字面量存储在只读内存
- 静态变量
static GLOBAL: i32 = 42;
fn main() {
let ref_global: &'static i32 = &GLOBAL; // 正确
}
函数生命周期标注
为了在函数签名中使用生命周期注解,需要在函数名和参数列表间的尖括号中声明泛型生命周期(lifetime)参数,就像泛型类型(type)参数一样。指定生命周期参数的正确方式依赖函数实现的具体功能。
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
- 函数或方法的参数的生命周期被称为 输入生命周期(input lifetimes),而返回值的生命周期被称为 输出生命周期(output lifetimes)。
结构体生命周期标注
在 Rust 中,当结构体(struct)包含引用类型的字段时,必须显式标注生命周期参数,以明确这些引用的有效作用域。
struct Movie<'a, 'b> {
name: &'a str,
director: &'b str,
}
fn main() {
let name = String::from("Inception");
let director = String::from("Christopher Nolan");
let movie = Movie {
name: &name,
director: &director,
};
// movie 的生命周期必须短于 name 和 director
}
枚举声明周期标注
在 Rust 中,当枚举(enum)包含引用类型的字段时,必须显式标注生命周期参数,以明确这些引用的有效作用域。
enum Msg<'a> {
Quit, // 可以是无数据类型的普通值
Move { x: i32, y: i32 }, // 可以是具名字段(结构体形式)
Text(&'a str), // 引用外部字符串
}
fn main() {
let quit = Msg::Quit;
let mov = Msg::Move { x: 10, y: 20 };
let text = Msg::Text("Hello");
}
方法声明周期标注
当为带有生命周期的结构体或枚举实现方法时,(实现方法时)结构体字段或枚举变体的生命周期必须总是在 impl 关键字之后声明并在结构体名称之后被使用,因为这些生命周期是结构体类型的一部分。
impl<'a> ImportantExcerpt<'a> {
fn level(&self) -> i32 {
3
}
}
与泛型参数及 trait bounds 结合
use std::fmt::Display;
fn longest_with_an_announcement<'a, T>(
x: &'a str,
y: &'a str,
ann: T,
) -> &'a str
where
T: Display,
{
println!("Announcement! {ann}");
if x.len() > y.len() {
x
} else {
y
}
}
参考
- https://kaisery.github.io/trpl-zh-cn/
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)