Rust:函数与控制流
在编程中,函数和控制流是构建程序逻辑的核心要素。Rust 在这些基础概念上引入了许多独特的设计,比如表达式化的控制结构、。本文将从最基础的概念开始,逐步深入探讨 Rust 中的条件表达式、循环控制、函数定义以及各种返回机制。
条件表达式
if分支
if 是最基本的条件控制结构,用于根据条件执行不同的代码分支:
rust
let number = 6;
if number > 5 {
println!("数字大于5");
} else if number < 0 {
println!("数字小于0");
} else {
println!("数字在 0 ~ 5 之间");
}
这是最传统的 if 用法,根据条件执行相应的代码块。当条件为真时,执行 if 后的代码块;当条件为假且有 else 分支时,执行 else 后的代码块。
if表达式化
与其他语言不同,Rust 中的 if 不仅是语句,更是表达式,可以返回值:
rust
let number = 6;
let result = if number % 2 == 0 {
"偶数"
} else {
"奇数"
};
println!("数字 {} 是 {}", number, result);
这里的关键概念是:大括号 {} 的最后一个表达式就是该代码块的返回值。注意 "偶数" 和 "奇数" 后面都没有分号,这意味着它们是表达式而不是语句,会作为代码块的返回值。
因此在Rust中没有三元表达式?:,因为if本身作为表达式就可以完成对应的功能。
例如以上代码在C++中可以用?:完成:
cpp
String str = number % 2 == 0 ? "偶数" : "奇数";
if表达式的类型要求
由于 if 是表达式,编译器需要在编译时确定其返回的类型,因此所有分支必须返回相同类型:
rust
let condition = true;
// 正确:所有分支返回相同类型
let number = if condition {
5
} else {
6
};
println!("number = {}", number);
// 错误示例(会编译失败):
let mixed = if condition {
5 // 整数类型
} else {
"six" // 字符串类型
};
这个限制确保了类型安全,编译器可以在编译时就确定变量的类型,避免运行时的类型错误。
match分支匹配
match 是 Rust 中更强大的分支控制结构,可以匹配多种模式:
rust
let number = 3;
match number {
1 => println!("一"),
2 => println!("二"),
3 => println!("三"),
_ => println!("其他"),
}
match 通过模式匹配来决定执行哪个分支。每个分支由模式和对应的代码组成,用 => 连接。_ 是通配符,匹配所有其他情况。
它类似于其他语言的switch语句,但是功能性远比switch强大。
match必须覆盖被匹配值的所有可能情况,这被称为"穷尽性"要求:
rust
enum Direction {
North,
South,
East,
West,
}
let dir = Direction::North;
match dir {
Direction::North => "向北前进",
Direction::South => "向南前进",
Direction::East => "向东前进",
Direction::West => "向西前进",
}
穷尽性检查是编译时进行的,确保你不会遗漏任何情况。这大大减少了运行时错误的可能性。
match支持多种复杂的模式匹配:
rust
let number = 7;
match number {
1 => println!("一"),
2 | 3 => println!("二或三"), // 多值匹配
4..=6 => println!("四到六"), // 范围匹配
_ => println!("其他"),
}
- 多值匹配:使用
|可以匹配多个值 - 范围匹配:使用
..=可以匹配一个范围内的值
模式匹配非常繁杂,功能非常强大,后续会开专门的章节讲解,本博客只是让大家了解存在这样一种分支处理的语法。
循环控制
Rust提供了三种循环表达式,分别是while、loop、for in,用法和其他语言基本类似。
loop
loop 创建一个无限循环,是最基本的循环结构:
rust
let mut counter = 0;
loop {
counter += 1;
println!("计数: {}", counter);
}
loop 会无限执行,是最直接的循环控制方式。当其内部不含任何break,那么整个表达式返回!类型。
break 和 continue
在循环中,break 用于退出循环,continue 用于跳过本次迭代:
rust
let mut count = 0;
loop {
count += 1;
if count % 2 == 0 {
continue; // 跳过偶数
}
if count > 10 {
break; // 超过10就退出
}
println!("奇数: {}", count);
}
continue 会跳过当前循环的剩余代码,直接进入下一次循环。break 会立即退出整个循环。
对于loop来说,break是一种常见的终止循环手段。那么当loop不是死循环,返回的值就不是!了,具体返回的值是什么取决于break。
break 返回值
循环也是表达式,可以通过 break 返回值,在break与分号;之间可以携带一个返回值。
rust
let mut num = 1;
let result = loop {
if num % 2 == 0 {
break num; // 返回 num 的值
}
num += 1;
};
println!("第一个偶数: {}", result);
这里 break num 表示退出循环并返回 num 的值。整个 loop 表达式的值就是 break 后面的值。
当break;的时候,也就是break不携带值,此时返回的是一个()单元类型。
while
while 循环格式如下:
rust
while 条件 {
// 循环体
}
在条件为真时持续执行循环体,否则就退出循环。
示例:
rust
let mut number = 5;
while number > 0 {
println!("倒计时: {}", number);
number -= 1;
}
println!("发射!");
while 在每次循环开始前检查条件,条件为假时退出循环。这比 loop + if + break 的组合更简洁。
while 循环中也可以使用 break 提前退出,但与 loop 不同的是,while 循环不允许 break 携带返回值:
rust
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 循环的条件可能一开始就为假,那样循环体根本不会执行,无法确定返回值。
有人可能就有疑问:已经有while了,为什么还需要loop?因为while true 不就可以实现死循环吗?
Rust一切皆表达式,表达式=副作用+返回值,loop与while true 的副作用类似,都是死循环,但是返回值有很大差别。不要用while true循环来代替loop实现死循环,本文后半部分讲到函数会给一个反例。
for in
for 循环用于遍历集合或迭代器,语法如下:
rust
for var in 迭代器 {
}
示例:
rust
let numbers = [1, 2, 3, 4, 5];
for num in numbers {
println!("数字: {}", num);
}
for 循环会自动遍历可迭代对象的每个元素。这是 Rust 中最常用的循环方式,既安全又高效。
in后可以放任何可遍历的迭代器,具体的深入原理会在后续的迭代器章节讲解。在这之前最常见的写法就是:
rust
for var in 数组名 {
}
能看懂即可,此处的var就是数组的每一个元素。
同样的,for in也是一个表达式,返回单元类型(),并且break不允许携带返回值==。
标签跳转
当有嵌套循环时,可以使用标签来指定 break 或 continue 要影响哪个循环,语法如下:
rust
'标签A loop {
'标签B while ... {
if ... {
break '标签A;
} else {
continue `标签A;
}
}
}
语法中,'标签A代表外层循环,'标签B代表内层循环,标签必须以单引号开头。在while 循环内部,可以通过break '标签A和continue '标签A 直接控制外层循环。
示例:
rust
'outer: loop {
println!("进入外层循环");
'inner: loop {
println!("进入内层循环");
break 'outer; // 跳出外层循环
}
println!("这行代码不会执行");
}
println!("退出所有循环");
这种标签这在复杂的嵌套循环中很有用,可以快速跳出多层循环。
当使用标签跳转时,break 也可以携带返回值,但只能在 loop 循环中使用:
rust
let result = 'outer: loop {
let mut inner_count = 0;
'inner: while inner_count < 5 {
inner_count += 1;
println!("内层计数: {}", inner_count);
if inner_count == 3 {
break 'outer inner_count; // 跳出外层循环并返回值
}
}
println!("这行代码不会执行");
};
println!("返回值: {}", result); // 输出: 返回值: 3
此处在 break 'outer的同时,携带了一个返回值inner_count,因为'outer是外层的loop循环,所以允许返回。
标签跳转的返回值只能用于 loop 循环,while 和 for 循环即使使用标签也不能返回值。
函数
对于一些重复执行的代码,可以将其定义为一个函数,方便调用。函数是封装代码逻辑的基本单位。
语法如下:
rust
fn 函数名(参数1: 类型1, 参数2: 类型2) {
// 函数体
}
函数通过关键字fn声明,在()内部的称为参数列表,每个参数以参数: 类型的形式声明。
后续在其他位置,就可以通过函数名(参数...)的形式调用这个函数。
rust
// 定义函数
fn print_num(num: i32) {
println!("Hello, {}!", num);
}
greet(100); // 调用函数
此处函数greet接受一个i32类型的参数,在函数内部会将该数字进行输出。
要注意,在Rust中不支持函数重载,也不支持参数缺省值。因为Rust设计中,认为功能不同的函数,最好用函数名就区分开,调用者可以不用纠结同一函数传入不同参数导致的不同行为,提高可靠性。
函数调用也算一种表达式,表达式就有值,尝试接受一下greet的值:
rust
let ret: () = greet(200);
可以看到,函数greet作为表达式返回了一个单元类型。如果想要函数表达式返回其它值,就需要用到函数返回值。
返回值
函数可以返回一个值,需要在参数列表后指定返回类型:
rust
fn 函数名(参数1: 类型1, 参数2: 类型2) -> 返回类型 {
// 函数体
}
使用 -> 指定返回类型,这样函数调用时就可以返回单元类型以外的类型了。
例如:
rust
fn add(a: i32, b: i32) -> i32 {
a + b
}
函数体{ }可以视为一个块表达式,{ }内最后一个表达式就是它的返回值,因此add函数会返回a + b 作为结果。
return
虽然 Rust 支持隐式返回,但有时需要在函数中间提前返回,此时就需要return关键字。
示例:
rust
fn check_positive(x: i32) -> i32 {
if x <= 0 {
return 0; // 提前返回
}
x * 2 // 正常返回
}
将要返回的值放在return关键字后面,就可以把这个值作为函数返回值,并终止函数。return 关键字可以在函数的任何位置提前返回值并退出函数。
参数模式匹配
函数参数可以使用模式匹配直接解构:
rust
// 解构元组参数
fn print_point((x, y): (i32, i32)) {
println!("坐标: ({}, {})", x, y);
}
// 解构数组的前几个元素
fn print_first_two([first, second, ..]: [i32; 5]) {
println!("前两个元素: {} 和 {}", first, second);
}
你看不懂以上内容也没关系,后续会有专门的章节降级模式匹配,包括函数参数中的模式匹配。此处你只需要知道函数参数是支持模式匹配的即可。
发散函数!
发散函数是永远不会正常返回的函数,返回类型是 !。
例如:
rust
fn infinite_loop() -> ! {
loop {
println!("永远运行...");
}
}
函数 infinite_loop 一旦进入就永远不会退出,因为内部是一个死循环loop。最后一个表达式loop返回的!作为函数返回值。此时 infinite_loop 就称为发散函数。
由于!可以转为任何类型,当然也就可以转化为单元类型(),因此省略返回值也可以:
rust
fn infinite_loop() {
loop {
println!("永远运行...");
}
}
panic!表示一个错误,而exit表示退出整个程序,这两个也会返回!类型。
对于一些有确定返回类型的函数,可以基于!的类型转化特性,很自然的进行报错处理:
rust
fn safe_divide(a: i32, b: i32) -> i32 {
if b == 0 {
panic!("除零错误") // ! 类型,但兼容 i32
} else {
a / b // i32 类型
}
// 整个 if 表达式的类型是 i32
}
函数safe_divide执行一个安全的除法,返回一个i32,这就要保证除数不为0。
通过if判断,如果b == 0就直接panic!,虽然函数要求返回一个i32,但是panic!依然可以作为函数的返回值,这得益于!的转化特性。
不过要注意,safe_divide不是一个发散函数,因为它是有可能会返回正常值的。
发散函数是指永远不会正常返回,它包括两种情况:
- 永远不返回,比如
loop死循环 - 只能非正常的返回,比如只调用
panic!和exit
rust
fn foo() -> ! {
panic!("This call never returns.");
}
以上就是一个发散函数。
函数屏蔽
在 Rust 中,作用域不仅能决定变量的生存周期,同时也决定了名字解析的优先级。这意味着一个同名的定义,可能会在局部范围内"屏蔽"掉外部的同名函数或变量。变量能被 shadowing,函数同样可以。
最简单的例子就是在函数内部重新定义一个同名函数:
rust
fn greet() {
println!("全局函数:你好!");
}
fn main() {
fn greet() {
println!("局部函数:嗨!");
}
greet(); // 调用的是局部定义,而不是全局的
}
名字解析遵循"就近原则",于是局部函数把全局的 greet 暂时遮挡住了。等离开 main 的作用域,全局的 greet 又会恢复可见。
编译期常量函数 CTFE
编译期常量函数(CTFE, Compile-Time Function Evaluation),让我们可以让某些函数在 编译阶段 就被执行,结果嵌入最终的程序中,而不是等到运行时去计算。
通过 const fn 关键字可以定义一个CTFE函数:
rust
const fn func(arg: type) -> type {
}
当传给func函数的参数是在编译期就可以确定的,那么func函数的结果也会在编译期提前计算,提高运行时效率。
示例:
rust
const fn square(x: i32) -> i32 {
x * x
}
const SIZE: usize = square(4) as usize;
fn main() {
let arr = [0; SIZE]; // 长度在编译期就确定为 16
println!("数组长度是 {}", arr.len());
}
square(4) 的结果在编译阶段就已经计算完毕,因此 arr 的大小在编译器眼中是一个确定的常量。
但是要注意,CTFE不代表它一定会在编译期完成计算。这还取决于传入的参数,例如:
rust
const fn func(num: i32) -> i32 {
num * 10
}
const C_NUM: i32 = 2;
const X: i32 = func(10);
const Y: i32 = func(C_NUM);
let num = 1;
const Z: i32 = func(num);
以上代码定义了一个func,它被const修饰,可以在编译期完成计算。
定义了一个常量C_NUM,它在编译期就可以完成计算。随后分别把字面量10和常量C_NUM作为参数传入func,再用const常量X和Y分别接收。由于字面量和常量在编译期都可以确定,所以X和Y不会报错。
后续定义了一个变量num,把它作为参数传给func,用Z接收。这行代码报错,无法编译,因为参数num不是一个编译期求值的变量。那么func就不会在编译期求值,而是在运行时调用。对于const Z常量要求必须在编译期拿到结果,于是发生冲突就报错了。