Rust:函数与控制流

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分支匹配

matchRust 中更强大的分支控制结构,可以匹配多种模式:

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提供了三种循环表达式,分别是whileloopfor 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一切皆表达式,表达式=副作用+返回值,loopwhile 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不允许携带返回值==。


标签跳转

当有嵌套循环时,可以使用标签来指定 breakcontinue 要影响哪个循环,语法如下:

rust 复制代码
'标签A loop {

	'标签B while ... {
		if ... {
			break '标签A;
		} else {
			continue `标签A;
		}
	}
	
}

语法中,'标签A代表外层循环,'标签B代表内层循环,标签必须以单引号开头。在while 循环内部,可以通过break '标签Acontinue '标签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 循环,whilefor 循环即使使用标签也不能返回值。


函数

对于一些重复执行的代码,可以将其定义为一个函数,方便调用。函数是封装代码逻辑的基本单位。

语法如下:

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不是一个发散函数,因为它是有可能会返回正常值的。

发散函数是指永远不会正常返回,它包括两种情况:

  1. 永远不返回,比如loop死循环
  2. 只能非正常的返回,比如只调用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常量XY分别接收。由于字面量和常量在编译期都可以确定,所以XY不会报错。

后续定义了一个变量num,把它作为参数传给func,用Z接收。这行代码报错,无法编译,因为参数num不是一个编译期求值的变量。那么func就不会在编译期求值,而是在运行时调用。对于const Z常量要求必须在编译期拿到结果,于是发生冲突就报错了。


相关推荐
豐儀麟阁贵4 小时前
5.4静态变量和静态方法
java·开发语言
枫叶丹44 小时前
【Qt开发】容器类控件(二)-> QTabWidget
开发语言·qt
qq_310658514 小时前
webrtc代码走读(七)-QOS-FEC-ulpfec rfc5109
网络·c++·webrtc
RTC老炮4 小时前
webrtc弱网-PccBitrateController类源码分析与算法原理
网络·算法·webrtc
草莓熊Lotso4 小时前
模板进阶:从非类型参数到分离编译,吃透 C++ 泛型编程的核心逻辑
linux·服务器·开发语言·c++·人工智能·笔记·后端
心无旁骛~4 小时前
Socket和Websocket编程的区别
网络·websocket·网络协议
草莓熊Lotso4 小时前
《算法闯关指南:优选算法--前缀和》--25.【模板】前缀和,26.【模板】二维前缀和
开发语言·c++·算法
浔川python社5 小时前
C++ 实际应用系列(第六部分):并发系统的性能优化与工程实践(完)
开发语言·c++·性能优化
tuokuac5 小时前
如何判断“IP+端口“通不通
网络·网络协议·tcp/ip