Rust:函数与控制流
在编程中,函数和控制流是构建程序逻辑的核心要素。Rust 在这些基础概念上引入了许多独特的设计,比如表达式化的控制结构、严格的类型系统以及所有权机制。本文将从最基础的概念开始,逐步深入探讨 Rust 中的条件表达式、循环控制、函数定义以及各种返回机制。
条件表达式
if分支语句
if
是最基本的条件控制结构,用于根据条件执行不同的代码分支:
let number = 6;
if number > 5 {
println!("数字大于5");
}
if number % 2 == 0 {
println!("数字是偶数");
} else {
println!("数字是奇数");
}
这是最传统的 if
用法,根据条件执行相应的代码块。当条件为真时,执行 if
后的代码块;当条件为假且有 else
分支时,执行 else
后的代码块。
if表达式化
与其他语言不同,Rust 中的 if
不仅是语句,更是表达式,可以返回值:
let number = 6;
let result = if number % 2 == 0 {
"偶数"
} else {
"奇数"
};
println!("数字 {} 是 {}", number, result);
这里的关键概念是:代码块 {}
的最后一个表达式就是该代码块的返回值。注意 "偶数"
和 "奇数"
后面都没有分号,这意味着它们是表达式而不是语句,会作为代码块的返回值。
if表达式的类型要求
由于 if
是表达式,编译器需要在编译时确定其类型,因此所有分支必须返回相同类型:
let condition = true;
// 正确:所有分支返回相同类型
let number = if condition {
5
} else {
6
};
println!("number = {}", number);
// 错误示例(会编译失败):
let mixed = if condition {
5 // 整数类型
} else {
"six" // 字符串类型
};
这个限制确保了类型安全,编译器可以在编译时就确定变量的类型,避免运行时的类型错误。
match分支匹配
match
是 Rust 中更强大的分支控制结构,可以匹配多种模式:
let number = 3;
match number {
1 => println!("一"),
2 => println!("二"),
3 => println!("三"),
_ => println!("其他"),
}
match
通过模式匹配来决定执行哪个分支。每个分支由模式和对应的代码组成,用 =>
连接。_
是通配符,匹配所有其他情况。
match的穷尽性要求
match
必须覆盖被匹配值的所有可能情况,这被称为"穷尽性"要求:
enum Direction {
North,
South,
East,
West,
}
let dir = Direction::North;
match dir {
Direction::North => "向北前进",
Direction::South => "向南前进",
Direction::East => "向东前进",
Direction::West => "向西前进",
// 如果缺少任何一个枚举值,编译器会报错
}
穷尽性检查是编译时进行的,确保你不会遗漏任何情况。这大大减少了运行时错误的可能性。
match的模式匹配
match
支持多种复杂的模式匹配:
let number = 7;
match number {
1 => println!("一"),
2 | 3 => println!("二或三"), // 多值匹配
4..=6 => println!("四到六"), // 范围匹配
_ => println!("其他"),
}
- 多值匹配:使用
|
可以匹配多个值 - 范围匹配:使用
..=
可以匹配一个范围内的值
模式守卫
模式守卫允许在匹配模式的基础上添加额外的条件:
let number = 15;
match number {
n if n > 20 => println!("大于20: {}", n),
n if n > 10 => println!("大于10但小于等于20: {}", n),
n if n > 0 => println!("大于0但小于等于10: {}", n),
0 => println!("恰好是零"),
_ => println!("负数或其他"),
}
模式守卫使用 变量 if 条件
的形式,在模式匹配成功后,还要满足 if
后的条件才会执行对应分支,如果不匹配,那么向下继续匹配。
match表达式返回值
与 if
类似,match
也是表达式,可以返回值:
let number = 3;
let description = match number {
1 => "一",
2 => "二",
3 => "三",
_ => "其他",
};
println!("数字 {} 对应 {}", number, description);
所有分支必须返回相同类型的值,这样整个 match
表达式才有确定的类型。
循环控制
loop无限循环
loop
创建一个无限循环,是最基本的循环结构:
let mut counter = 0;
loop {
counter += 1;
println!("计数: {}", counter);
if counter == 5 {
break;
}
}
println!("循环结束");
loop
会无限执行,只有通过 break
才能退出循环。这是最直接的循环控制方式,适用于需要明确控制退出条件的场景。
break和continue
在循环中,break
用于退出循环,continue
用于跳过本次迭代:
let mut count = 0;
loop {
count += 1;
if count % 2 == 0 {
continue; // 跳过偶数
}
if count > 10 {
break; // 超过10就退出
}
println!("奇数: {}", count);
}
continue
会跳过当前循环的剩余代码,直接进入下一次循环。break
会立即退出整个循环。
loop表达式返回值
loop
也是表达式,可以通过 break
返回值:
let mut num = 1;
let result = loop {
if num % 2 == 0 {
break num; // 返回 num 的值
}
num += 1;
};
println!("第一个偶数: {}", result);
这里 break num
表示退出循环并返回 num
的值。整个 loop
表达式的值就是 break
后面的值。
while条件循环
while
循环在条件为真时持续执行:
let mut number = 5;
while number > 0 {
println!("倒计时: {}", number);
number -= 1;
}
println!("发射!");
while
在每次循环开始前检查条件,条件为假时退出循环。这比 loop
+ if
+ break
的组合更简洁。
while循环中的break
while
循环中也可以使用 break
提前退出,但与 loop
不同的是,while
循环不允许 break
携带返回值:
let mut count = 0;
while count < 10 {
count += 1;
if count == 5 {
break; // 可以 break,但不能 break 值
}
println!("计数: {}", count);
}
let result = while condition { break 42; }; // 错误!while 不能返回值
这是因为 while
循环的条件可能一开始就为假,那样循环体根本不会执行,无法确定返回值。
for迭代器循环
for
循环用于遍历集合或迭代器:
let numbers = [1, 2, 3, 4, 5];
for num in numbers {
println!("数字: {}", num);
}
for
循环会自动遍历可迭代对象的每个元素。这是 Rust 中最常用的循环方式,既安全又高效。
for循环获取索引
有时需要在遍历时获取元素的索引:
let fruits = ["苹果", "香蕉", "橙子"];
for (index, fruit) in fruits.iter().enumerate() {
println!("{}: {}", index, fruit);
}
enumerate()
方法返回 (索引, 元素)
的元组,让你可以同时访问索引和元素值。
标签跳转
当有嵌套循环时,可以使用标签来指定 break
或 continue
要影响哪个循环:
'outer: loop {
println!("进入外层循环");
'inner: loop {
println!("进入内层循环");
break 'outer; // 跳出外层循环
}
println!("这行代码不会执行");
}
println!("退出所有循环");
标签以单引号开头,可以让 break
和 continue
作用于指定的循环。这在复杂的嵌套循环中很有用。
当使用标签跳转时,break
也可以携带返回值,但只能在 loop
循环中使用:
let result = 'outer: loop {
let mut inner_count = 0;
'inner: loop {
inner_count += 1;
println!("内层计数: {}", inner_count);
if inner_count == 3 {
break 'outer inner_count * 10; // 跳出外层循环并返回值
}
if inner_count > 5 {
break 'inner; // 只跳出内层循环
}
}
println!("这行代码不会执行");
};
println!("返回值: {}", result); // 输出: 返回值: 30
标签跳转的返回值只能用于 loop
循环,while
和 for
循环即使使用标签也不能返回值。
函数签名与返回值
基本函数定义
函数是封装代码逻辑的基本单位:
fn greet(name: &str) {
println!("Hello, {}!", name);
}
greet("World");
函数使用 fn
关键字定义,函数名后跟参数列表,参数需要指定类型。
带返回值的函数
函数可以返回值,需要在参数列表后指定返回类型:
fn add(a: i32, b: i32) -> i32 {
a + b
}
let result = add(5, 3);
println!("5 + 3 = {}", result);
使用 ->
指定返回类型,函数体最后一个表达式作为返回值。
对于包含多个分支的函数,每个分支的最后一个表达式都可以作为该分支的返回值:
fn calculate(x: i32) -> i32 {
let doubled = x * 2;
if doubled > 10 {
doubled - 5 // 这个分支的返回值
} else {
doubled + 5 // 这个分支的返回值
}
// 整个 if 表达式的值作为函数返回值
}
fn classify_number(n: i32) -> &'static str {
match n {
0 => "零", // 这个分支的返回值
1..=10 => "小数", // 这个分支的返回值
11..=100 => "中数", // 这个分支的返回值
_ => "大数", // 这个分支的返回值
}
// 整个 match 表达式的值作为函数返回值
}
函数体最后一个表达式(这里是整个 if
或 match
表达式)自动作为返回值,不需要显式的 return
。
return
虽然 Rust 支持隐式返回,但有时需要在函数中间提前返回:
fn check_positive(x: i32) -> i32 {
if x <= 0 {
return 0; // 提前返回
}
x * 2 // 正常返回
}
return
关键字可以在函数的任何位置提前返回值并退出函数。
单元类型()
当函数不需要返回有意义的值时,返回单元类型 ()
:
fn print_info(msg: &str) {
println!("信息: {}", msg);
// 隐式返回 ()
}
fn do_something() -> () {
println!("执行某些操作");
// 显式指定返回 (),但通常不需要
}
单元类型 ()
表示"无意义的值",类似于其他语言中的 void
。不返回值的函数实际上返回 ()
。
参数传递
函数可以接受参数:
fn display_info(name: &str, age: i32, height: f64) {
println!("姓名: {}", name);
println!("年龄: {} 岁", age);
println!("身高: {:.1} 米", height);
}
fn calculate_area(width: f64, height: f64) -> f64 {
width * height
}
函数参数按顺序传递,每个参数都必须指定类型。
数组参数
可以将数组作为参数传递:
fn print_array(arr: [i32; 5]) {
for element in arr {
println!("{}", element);
}
}
fn sum_array(arr: [i32; 3]) -> i32 {
let mut total = 0;
for element in arr {
total += element;
}
total
}
fn main() {
let numbers = [1, 2, 3, 4, 5];
print_array(numbers);
let small_array = [10, 20, 30];
let sum = sum_array(small_array);
println!("数组总和: {}", sum);
}
数组参数需要指定确切的大小,例如 [i32; 5]
表示包含5个i32元素的数组。
参数模式匹配
函数参数可以使用模式匹配直接解构:
// 解构元组参数
fn print_point((x, y): (i32, i32)) {
println!("坐标: ({}, {})", x, y);
}
// 解构数组的前几个元素
fn print_first_two([first, second, ..]: [i32; 5]) {
println!("前两个元素: {} 和 {}", first, second);
}
参数模式匹配让你可以直接在函数签名中解构复杂类型,使代码更简洁。
发散函数!
发散函数是永远不会正常返回的函数,返回类型是 !
:
fn crash() -> ! {
panic!("程序崩溃了");
}
fn infinite_loop() -> ! {
loop {
println!("永远运行...");
}
}
!
类型表示"never",意味着函数永远不会正常返回。常见的发散函数包括 panic!
、无限循环等。
类型兼容性
一般来说,我们不会把!
直接作为返回值,这个类型的特殊之处在于它可以强制转换为任何类型:
fn safe_divide(a: i32, b: i32) -> i32 {
if b == 0 {
handle_error("除零错误"); // ! 类型,但兼容 i32
} else {
a / b // i32 类型
}
// 整个 if 表达式的类型是 i32
}
!
类型可以与任何类型兼容,这使得在某些分支中使用 panic!
或 exit
变得很自然,哪怕函数要求返回 i32
等其他类型,你也可以返回一个 !
来表示一个错误。