流程控制和函数是任何编程语言的核心组成部分,Rust 也不例外。
本章介绍了 Rust 中的流程控制与函数相关知识,包括:
- 条件判断:
if-else
结构和if let
模式匹配 - 循环结构:
loop
(无限循环)、while
(条件循环)和for
(迭代循环) - 函数:定义、参数、返回值和函数指针的使用
- 函数高级特性:默认参数的模拟、可变参数的实现和递归函数
- 代码块与作用域:作用域规则和变量遮蔽特性
一、条件判断(if-else、if let)
Rust 的条件判断用于根据不同条件执行不同代码分支,主要包括if-else
结构和if let
模式匹配。
1.1 if-else 基本用法
Rust 的if
条件不需要用括号()
包裹,但其代码块必须用{}
包围。条件表达式的结果必须是bool
类型(Rust 不会自动进行类型转换)。
fn main() {
let age = 18;
if age >= 18 {
println!("已成年");
} else if age >= 13 {
println!("青少年");
} else {
println!("儿童");
}
// 错误示例:条件必须是bool类型
// let num = 5;
// if num { // 编译错误:expected `bool`, found `i32`
// println!("num is non-zero");
// }
}
if
表达式可以作为赋值语句的一部分,因为 Rust 中一切都是表达式(有返回值):
fn main() {
let score = 85;
let grade = if score >= 90 {
'A'
} else if score >= 80 {
'B'
} else {
'C'
};
println!("成绩等级: {}", grade); // 输出:成绩等级: B
}
1.2 if let 模式匹配
if let
是一种简化的模式匹配,适用于只关心一种匹配情况的场景,比match
表达式更简洁。常用于处理Option
类型或枚举。
fn main() {
// 处理Option类型
let some_value: Option<i32> = Some(5);
// 传统match方式
match some_value {
Some(x) => println!("找到值: {}", x),
None => (), // 不关心None的情况,只做占位
}
// 等价的if let方式
if let Some(x) = some_value {
println!("找到值: {}", x); // 输出:找到值: 5
}
// 结合else处理不匹配的情况
let another_value: Option<i32> = None;
if let Some(x) = another_value {
println!("找到值: {}", x);
} else {
println!("没有值"); // 输出:没有值
}
// 处理枚举
enum Fruit {
Apple,
Banana(String),
}
let fruit = Fruit::Banana("黄香蕉".to_string());
if let Fruit::Banana(color) = fruit {
println!("这是{}的香蕉", color); // 输出:这是黄香蕉的香蕉
}
}
二、循环结构(loop、while、for 与迭代器)
Rust 提供了三种循环结构:loop
(无限循环)、while
(条件循环)和for
(迭代循环),其中for
循环与迭代器结合使用最为常见。
2.1 loop 无限循环
loop
创建一个无限循环,必须使用break
才能退出。它的特殊之处是可以返回一个值。
fn main() {
// 基本用法
let mut count = 0;
loop {
count += 1;
if count == 3 {
println!("计数到3,退出循环"); // 输出:计数到3,退出循环
break; // 退出循环
}
}
// 带返回值的loop
let mut sum = 0;
let result = loop {
sum += 5;
if sum >= 20 {
break sum; // 返回sum的值
}
};
println!("循环返回值: {}", result); // 输出:循环返回值: 20
}
2.2 while 条件循环
while
循环在条件为true
时持续执行,适合不确定循环次数的场景。
fn main() {
let mut number = 3;
while number > 0 {
println!("{}!", number);
number -= 1;
}
println!("发射!");
// 输出:
// 3!
// 2!
// 1!
// 发射!
}
2.3 for 循环与迭代器
for
循环是 Rust 中最常用的循环方式,通常与迭代器配合使用,用于遍历集合或范围。
fn main() {
// 遍历范围(左闭右开)
for i in 1..5 { // 1到4(不包含5)
println!("{}", i); // 输出:1 2 3 4
}
// 遍历数组
let fruits = ["苹果", "香蕉", "橙子"];
for fruit in fruits {
println!("水果: {}", fruit);
}
// 使用迭代器方法enumerate获取索引和值
for (index, value) in fruits.iter().enumerate() {
println!("索引{}: {}", index, value);
// 输出:
// 索引0: 苹果
// 索引1: 香蕉
// 索引2: 橙子
}
// 遍历集合(需要引入标准库)
use std::collections::VecDeque;
let mut queue = VecDeque::new();
queue.push_back("a");
queue.push_back("b");
for item in queue {
println!("队列元素: {}", item);
}
}
Rust 的迭代器提供了丰富的方法(如map
、filter
、collect
等),可以进行复杂的数据处理:
fn main() {
let numbers = 1..10;
// 过滤偶数并乘以2
let result: Vec<i32> = numbers
.filter(|x| x % 2 == 0)
.map(|x| x * 2)
.collect();
println!("处理结果: {:?}", result); // 输出:处理结果: [4, 8, 12, 16]
}
三、函数定义与调用(参数、返回值、函数指针)
函数是代码复用和逻辑组织的基本单元,Rust 中使用fn
关键字定义函数。
3.1 函数定义与调用
Rust 函数的基本结构:fn 函数名(参数列表) -> 返回值类型 { 函数体 }
// 定义一个简单函数
fn greet(name: &str) {
println!("Hello, {}!", name);
}
// 带返回值的函数
fn add(a: i32, b: i32) -> i32 {
a + b // 表达式末尾没有分号,作为返回值
}
// 多个返回值(通过元组实现)
fn split_name(full_name: &str) -> (&str, &str) {
let parts: Vec<&str> = full_name.split_whitespace().collect();
(parts[0], parts[1]) // 返回元组
}
fn main() {
// 调用函数
greet("Rust"); // 输出:Hello, Rust!
let sum = add(3, 5);
println!("3 + 5 = {}", sum); // 输出:3 + 5 = 8
let (first, last) = split_name("John Doe");
println!("名: {}, 姓: {}", first, last); // 输出:名: John, 姓: Doe
}
注意:
函数参数必须指定类型
函数返回值通过->指定类型
函数体中最后一个表达式(不带分号)作为返回值,也可以使用return关键字提前返回
3.2 函数参数的可变性
Rust 函数参数默认是不可变的,若要在函数内修改参数,需要显式声明mut
:
fn main() {
let mut x = 5;
modify_value(&mut x); // 传递可变引用
println!("修改后的值: {}", x); // 输出:修改后的值: 10
}
// 接收可变引用参数
fn modify_value(num: &mut i32) {
*num *= 2; // 使用*解引用
}
3.3 函数指针
函数指针(function pointer)允许将函数作为参数传递或存储在变量中,类型表示为fn(参数类型) -> 返回值类型
。
// 定义几个数学函数
fn add(a: i32, b: i32) -> i32 {
a + b
}
fn subtract(a: i32, b: i32) -> i32 {
a - b
}
// 接收函数指针作为参数
fn calculate(operation: fn(i32, i32) -> i32, x: i32, y: i32) -> i32 {
operation(x, y)
}
fn main() {
let a = 10;
let b = 5;
// 传递函数作为参数
let sum = calculate(add, a, b);
let diff = calculate(subtract, a, b);
println!("{} + {} = {}", a, b, sum); // 输出:10 + 5 = 15
println!("{} - {} = {}", a, b, diff); // 输出:10 - 5 = 5
// 存储函数指针
let op: fn(i32, i32) -> i32 = add;
println!("使用函数指针: {}", op(3, 4)); // 输出:7
}
四、函数的高级特性(默认参数、可变参数、递归)
Rust 的函数系统提供了一些高级特性,虽然有些特性的实现方式与其他语言不同,但能满足复杂的编程需求。
4.1 默认参数
Rust没有内置的默认参数语法,但可以通过以下方式模拟:
使用结构体 + 默认实现
// 定义配置结构体
#[derive(Default)]
struct Config {
timeout: u32,
retries: u8,
}
// 接收配置参数的函数
fn connect(config: Config) {
println!("连接配置 - 超时: {}ms, 重试次数: {}", config.timeout, config.retries);
}
fn main() {
// 使用默认配置
connect(Config::default()); // 输出:连接配置 - 超时: 0ms, 重试次数: 0
// 自定义部分配置
connect(Config {
timeout: 5000,
..Config::default() // 其余使用默认值
}); // 输出:连接配置 - 超时: 5000ms, 重试次数: 0
}
使用函数重载
// 不使用 derive(Default),而是手动实现
struct Config {
timeout: u32,
retries: u8,
}
// 手动实现 Default trait,定义自定义默认值
impl Default for Config {
fn default() -> Self {
Config {
timeout: 5000, // 自定义默认超时为 5000ms
retries: 3, // 自定义默认重试次数为 3 次
}
}
}
// 接收配置参数的函数
fn connect(config: Config) {
println!("连接配置 - 超时: {}ms, 重试次数: {}", config.timeout, config.retries);
}
fn main() {
// 使用默认配置
connect(Config::default()); // 输出:连接配置 - 超时: 0ms, 重试次数: 0
// 自定义部分配置
connect(Config {
timeout: 1000,
..Config::default() // 其余使用默认值
}); // 输出:连接配置 - 超时: 5000ms, 重试次数: 0
}
4.2 定义接口
在 Rust 中,trait
(特质)是一种定义方法集合的机制,类似于其他语言中的 “接口(interface)”,但功能更灵活。它主要用于:
- 定义一组行为(方法)的规范,要求实现者必须提供这些行为的具体逻辑;
- 实现代码复用和多态(同一接口的不同实现)。
通俗理解:trait
是 “行为规范”
可以把 trait
想象成一份 “协议”,它规定了 “实现者必须具备哪些能力(方法)”。例如,Greet
trait 定义了 “打招呼” 的能力,任何类型(结构体、枚举等)只要只要实现 Greet
,就必须提供 greet
方法的具体实现。
基本用法示例
// 定义一个 trait(行为规范):要求实现者必须有 greet 方法
trait Greet {
// 声明方法(只定义签名,不写实现)
fn greet(&self);
}
// 定义一个结构体(实现者1)
struct Person {
name: String,
}
// 为 Person 实现 Greet trait(遵守规范)
impl Greet for Person {
fn greet(&self) {
println!("Hello, I'm {}", self.name);
}
}
// 定义另一个结构体(实现者2)
struct Robot {
model: String,
}
// 为 Robot 实现 Greet trait(不同实现)
impl Greet for Robot {
fn greet(&self) {
println!("Greetings, human. I am {}", self.model);
}
}
fn main() {
let alice = Person { name: "Alice".to_string() };
let r2d2 = Robot { model: "R2-D2".to_string() };
// 调用各自的 greet 方法(多态:同一方法名,不同实现)
alice.greet(); // 输出:Hello, I'm Alice
r2d2.greet(); // 输出:Greetings, human. I am R2-D2
}
trait
的核心特性
1.强制实现:
任何类型要使用 trait
,必须实现其所有方法(除非方法有默认实现)。
2.默认方法:
可以在 trait
中直接提供方法的默认实现,实现者可以选择覆盖或直接使用:
trait Greet {
// 带默认实现的方法
fn greet(&self) {
println!("Hello!");
}
}
struct Cat;
struct Dog;
// 不覆盖默认方法,直接使用
impl Greet for Cat {}
// 覆盖默认方法
impl Greet for Dog {
fn greet(&self) {
println!("Hello! Dog!");
}
}
fn main() {
Cat.greet(); // 输出:Hello!
Dog.greet(); // 输出:Hello! Dog!
}
3. 规范参数 / 返回值:
可以用 trait
约束函数的参数或返回值,实现 “泛型多态”:
// 定义一个 trait(行为规范):要求实现者必须有 greet 方法
trait Greet {
// 声明方法(只定义签名,不写实现)
fn greet(&self);
}
// 定义一个结构体(实现者1)
struct Person {
name: String,
}
// 为 Person 实现 Greet trait(遵守规范)
impl Greet for Person {
fn greet(&self) {
println!("Hello, I'm {}", self.name);
}
}
// 定义另一个结构体(实现者2)
struct Robot {
model: String,
}
// 为 Robot 实现 Greet trait(不同实现)
impl Greet for Robot {
fn greet(&self) {
println!("Greetings, human. I am {}", self.model);
}
}
// 接受任何实现了 Greet 的类型
fn say_hello(who: &impl Greet) {
who.greet();
}
fn main() {
let alice = Person { name: "Alice".to_string() };
let r2d2 = Robot { model: "R2-D2".to_string() };
// 调用各自的 greet 方法(多态:同一方法名,不同实现)
alice.greet(); // 输出:Hello, I'm Alice
r2d2.greet(); // 输出:Greetings, human. I am R2-D2
say_hello(&alice); // 合法(Person 实现了 Greet)
say_hello(&r2d2); // 合法(Robot 实现了 Greet)
}
4. 与 “接口” 的区别
虽然类似其他语言的接口,trait
有更灵活的特性:
- 可以为任何类型实现
trait
(包括标准库类型,如i32
、String
等); - 支持 “默认方法”,减少重复代码;
- 可以通过
trait
组合实现复杂行为。
总之,trait
是 Rust 中定义行为规范的核心机制,它:
- 规定了类型必须实现的方法;
- 支持多态和代码复用;
- 是 Rust 实现 “接口抽象” 的主要方式。
通过 trait
,你可以写出更灵活、可扩展的代码,尤其是在需要处理多种类型但统一行为的场景(如通用算法、工具函数等)。
4.3 可变参数
Rust 支持可变参数(variable arguments),通常通过宏或标准库中的std::fmt::Arguments
实现:
1. 使用宏定义可变参数函数
// 定义可变参数宏
macro_rules! sum {
// 递归终止条件
($x:expr) => ($x);
// 递归处理多个参数
($x:expr, $($y:expr),+) => ($x + sum!($($y),+));
}
fn main() {
let s1 = sum!(1, 2, 3);
let s2 = sum!(10, 20, 30, 40);
println!("sum1: {}, sum2: {}", s1, s2); // 输出:sum1: 6, sum2: 100
}
2. 使用std::fmt
模块处理可变参数(类似println!
)
use std::fmt;
fn log(fmt: fmt::Arguments) {
println!("[LOG] {}", fmt);
}
fn main() {
let user = "admin";
let action = "login";
// 使用format_args!创建可变参数
log(format_args!("用户 {} 执行了 {} 操作", user, action));
// 输出:[LOG] 用户 admin 执行了 login 操作
}
4.4 递归函数
递归函数是调用自身的函数,在 Rust 中使用递归需要注意:
- 必须有明确的终止条件
- Rust 目前不支持尾递归优化,递归深度过大会导致栈溢出
// 计算阶乘(n! = n × (n-1) × ... × 1)
fn factorial(n: u64) -> u64 {
// 终止条件
if n <= 0 {
1
} else {
n * factorial(n - 1) // 递归调用
}
}
// 斐波那契数列(F(n) = F(n-1) + F(n-2))
fn fibonacci(n: u32) -> u32 {
match n {
0 => 0,
1 => 1,
_ => fibonacci(n - 1) + fibonacci(n - 2), // 递归调用
}
}
fn main() {
println!("5的阶乘: {}", factorial(5)); // 输出:5的阶乘: 120
println!("斐波那契数列第10项: {}", fibonacci(10)); // 输出:55
}
对于深度较大的递归,建议使用循环替代,避免栈溢出:
// 用循环实现阶乘(更安全)
fn factorial_iterative(n: u64) -> u64 {
let mut result = 1;
for i in 1..=n {
result *= i;
}
result
}
fn main() {
println!("5的阶乘: {}", factorial_iterative(5)); // 输出:5的阶乘: 120
}
五、代码块与作用域({} 作用域、变量遮蔽 Shadowing)
Rust 通过代码块(block)和作用域(scope)管理变量的生命周期,变量遮蔽是 Rust 中一个有特色的概念。
5.1 代码块与作用域
代码块用{}
包围,创建一个新的作用域。变量在声明的作用域内有效,出了作用域会被自动释放(RAII 机制)。
fn main() {
// 外部作用域
let x = 10;
println!("外部作用域的x: {}", x); // 输出:外部作用域的x: 10
{
// 内部作用域
let y = 20;
println!("内部作用域的y: {}", y); // 输出:内部作用域的y: 20
println!("内部作用域可以访问外部变量x: {}", x); // 输出:10
}
// 错误:y的作用域已结束
// println!("外部作用域无法访问y: {}", y); // 编译错误
// 控制流结构(if/loop/for等)也会创建作用域
if x > 5 {
let z = 30;
println!("if块内的z: {}", z); // 输出:if块内的z: 30
}
// 错误:z的作用域已结束
// println!("if块外无法访问z: {}", z); // 编译错误
}
5.2 变量遮蔽(Shadowing)
变量遮蔽允许在同一作用域或内层作用域中用相同名称声明新变量,新变量会 "遮蔽" 旧变量。
fn main() {
let x = 5;
println!("x = {}", x); // 输出:x = 5
// 遮蔽:同一作用域中创建同名新变量
let x = x + 1;
println!("x = {}", x); // 输出:x = 6
// 遮蔽:改变变量类型
let x = "现在我是字符串";
println!("x = {}", x); // 输出:x = 现在我是字符串
// 内层作用域遮蔽外层变量
{
let x = 100;
println!("内层作用域的x = {}", x); // 输出:内层作用域的x = 100
}
// 内层作用域结束后,外层变量恢复可见
println!("外层作用域的x = {}", x); // 输出:外层作用域的x = 现在我是字符串
}
变量遮蔽的主要用途:
- 转换变量类型同时保持名称一致
- 临时修改变量值但不影响外部作用域
- 简化代码(无需为相关变量创建不同名称)