第 03 篇
函数与控制流程
学习函数、分支和循环,解释块与分号的结果类型、单元值、提前返回、循环标签和短路求值。
Hex4C59 15 分钟阅读 更新于 2026-10-17
编程·原创·实战教程
前两节里,你已经运行过程序,也学会了保存数据和计算。本节会把重复的动作写成可以多次调用的函数,再让程序按条件选择分支、依次处理记录。
具体来说,用函数组织计算,用 if 作判断,用 for、while 和 loop 重复操作,最后完成一个三天学习统计程序。本节的函数只使用数字、布尔值和固定长度数组,借用与生命周期留到相应章节。
继续在 hello-rust 项目里练习。完整程序可以直接替换 src/main.rs,运行 cargo run;只有明确标注的替换行或局部片段需要放入已有代码。
3.1 从一个没有参数的函数开始 #
完整程序:
fn print_goal() {
println!("今天的目标:练习函数。");
}
fn main() {
print_goal();
print_goal();
}输出两次同样的目标。
fn print_goal() { ... } 定义函数,说明这个动作怎样完成。print_goal(); 调用函数,执行里面的代码。main 先执行第一条调用,完成后再执行第二条。
定义一个函数不会自动运行它。在 main 中删掉两条调用,运行后就不会打印目标;编译器还可能提醒这个函数没有使用。
函数名通常也使用小写和下划线,例如 total_minutes。函数可以定义在 main 前面或后面。对这里的普通函数,执行顺序取决于调用位置,与定义在文件中的先后无关。
3.2 参数让同一段逻辑处理不同输入 #
刚才的函数每次打印相同的目标。要让它打印不同的分钟数,可以增加参数。完整程序:
fn print_minutes(minutes: u32) {
println!("这次学习 {minutes} 分钟");
}
fn main() {
print_minutes(20);
print_minutes(45);
}预期输出:
这次学习 20 分钟
这次学习 45 分钟函数定义中的 minutes: u32 是参数,表示函数接收一个 u32 值。调用 print_minutes(20) 时,20 就是这次调用的实际输入。
函数参数需要写明类型。函数内部的 minutes 只属于这次函数调用的局部范围;它不会自动改变 main 中的同名变量。
多个参数用逗号分隔,调用时按对应顺序提供值。参数名写清楚,可以减少把两个同类型数字传反的错误。
3.3 返回值把计算结果交回调用方 #
打印结果和返回结果是两件事。一个加法函数可以只负责算出和,调用方再决定打印、继续计算或作比较。
完整程序:
fn add(left: i32, right: i32) -> i32 {
left + right
}
fn main() {
let total = add(20, 30);
let doubled = total * 2;
println!("合计:{total},两倍:{doubled}");
}输出 合计:50,两倍:100。
拆开函数签名:
| 部分 | 含义 |
|---|---|
fn add | 定义名为 add 的函数 |
(left: i32, right: i32) | 接收两个 i32 参数 |
-> i32 | 返回一个 i32 值 |
left + right | 计算并作为函数结果交回 |
调用时,20 和 30 传入函数,计算结果 50 通过 let total = ... 赋给变量。函数本身不打印,终端里的输出来自 main。
3.4 返回值后面为什么没有分号 #
Rust 中,表达式会计算出一个值。把表达式写成以分号结束的语句时,通常只执行它的动作,丢弃它产生的值。函数体最后的表达式可以成为返回值,因此刚才的 left + right 后面没有分号。
下面是故意无法编译的完整程序:
fn add(left: i32, right: i32) -> i32 {
left + right;
}
fn main() {
println!("{}", add(20, 30));
}加上分号后,加法的结果被丢弃,不再作为函数体的值返回。函数体没有留下要交出的整数,得到的是单元值 (),表示这里没有额外结果。它与声明的 i32 不匹配,编译器通常报告 E0308。
可以去掉这个分号,也可以明确写 return。下面是后一种写法的完整程序:
fn add(left: i32, right: i32) -> i32 {
return left + right;
}
fn main() {
println!("{}", add(20, 30));
}return 会立即结束当前函数并返回结果。通常把返回结果的表达式直接放在函数末尾,需要提前结束时再用 return。是否加分号,要看这段代码是否需要产生值,不能每行都加。
没有声明 -> ... 的普通函数默认返回 ()。print_minutes 只执行打印动作,就属于这种情况。
3.5 用 if 选择执行路径 #
完整程序:
fn main() {
let minutes = 35;
if minutes >= 60 {
println!("学习时间充足");
} else if minutes >= 30 {
println!("已经完成一轮练习");
} else {
println!("再安排一小段时间");
}
}35 不满足第一个条件,但满足第二个,所以只输出 已经完成一轮练习。
if 按顺序检查条件。一旦选中一个分支,就跳过同一组的其他分支。这里如果先判断 minutes >= 30,再判断 minutes >= 60,60 以上也会先进入第一个分支;分支顺序会影响结果。
条件必须产生 bool。if minutes >= 60 合法,因为比较返回真或假;if minutes 不合法,Rust 不会把非零整数自动当成真。
把 minutes 依次改成 29、30、59、60,预测输出,再运行。边界值能帮助你检查应该使用 > 还是 >=。
3.6 if 也可以计算出一个值 #
完整程序:
fn main() {
let minutes = 35;
let target = 30;
let message = if minutes >= target {
"已达标"
} else {
"未达标"
};
println!("学习了 {minutes} 分钟:{message}");
}被选中的分支产生的文本会赋给 message。分支末尾没有分号,因为这里需要它们的值;整个 let message = ...; 最后有分号,表示变量声明结束。
两个分支必须产生兼容类型的值。如果一个产生文本,另一个产生整数,就不能赋给这里的同一个变量。
只需要执行动作时,可以像上一小节那样在分支中打印;后面还要使用选择结果时,就让 if 产生值。
3.7 用 for 依次处理数组元素 #
沿用第二节的数组写法,用 [20, 30, 40] 保存三天记录。如果要把每项相加,不必手写三个索引,可以用 for。
完整程序:
fn main() {
let days = [20, 30, 40];
let mut total = 0;
for minutes in days {
total += minutes;
println!("刚加入 {minutes} 分钟,目前合计 {total}");
}
println!("最终合计:{total} 分钟");
}预期输出:
刚加入 20 分钟,目前合计 20
刚加入 30 分钟,目前合计 50
刚加入 40 分钟,目前合计 90
最终合计:90 分钟for minutes in days 每次把下一个元素交给本轮的 minutes。循环执行三轮,分别处理 20、30、40;没有下一个元素时退出。
在循环外初始化 total,并声明为 mut,才能让每一轮接着上一次的结果累加。如果把 let mut total = 0; 放进循环,每轮都会重新从 0 开始,得不到整组数据的总和。
先用 for 遍历数据本身,可以减少手写索引和长度判断。第四、五节再讨论遍历时怎样借用或移动数据;这里使用简单整数数组。
3.8 用范围生成连续整数 #
完整程序:
fn main() {
for day in 1..=3 {
println!("第 {day} 天");
}
for number in 1..3 {
println!("不包含右边界:{number}");
}
}第一段输出第 1、2、3 天;第二段只输出 1、2。
| 范围 | 生成的整数 |
|---|---|
0..3 | 0、1、2 |
1..3 | 1、2 |
1..=3 | 1、2、3 |
.. 不包含右边界,..= 包含右边界。不能直接把范围的上界当成执行次数,还要看起点。
例如要打印 1~5,就写 1..=5。遍历数组时,本节优先使用 for value in array,不用为了练习范围再改成索引。
3.9 while 每轮先检查条件 #
完整程序:
fn main() {
let mut remaining = 3;
while remaining > 0 {
println!("倒计时:{remaining}");
remaining -= 1;
}
println!("结束时:{remaining}");
}输出倒计时 3、2、1,最后是 结束时:0。
每轮先检查 remaining > 0,为真才执行循环体,然后再检查条件:
| 检查时的值 | 条件是否成立 | 本轮动作 |
|---|---|---|
| 3 | 是 | 打印 3,减成 2 |
| 2 | 是 | 打印 2,减成 1 |
| 1 | 是 | 打印 1,减成 0 |
| 0 | 否 | 跳出循环 |
如果一开始就是 0,循环体一次也不会执行。如果删掉 remaining -= 1,条件就会一直成立,程序不停打印。遇到这种情况,可以在终端按 Ctrl+C 中断,再检查循环条件或变量更新。
3.10 loop 持续执行,直到 break #
完整程序:
fn main() {
let mut attempts = 0;
loop {
attempts += 1;
println!("第 {attempts} 次尝试");
if attempts == 3 {
break;
}
}
println!("已经退出循环");
}程序输出第 1、2、3 次尝试后退出循环。loop 没有自带条件,需要通过 break 或其他控制流结束。本例把条件写在循环体里。
break 只退出当前循环,不等于退出整个函数;所以最后一行还会打印。普通 return 则结束当前函数。
loop 还可以在退出时交出值。完整程序:
fn main() {
let mut attempts = 0;
let result = loop {
attempts += 1;
if attempts == 2 {
break attempts * 10;
}
};
println!("循环结果:{result}");
}这里用 break attempts * 10 在退出时提供值 20,赋给外面的 result。退出时带值的写法适用于 loop,不能直接照搬成普通 for 或 while 的 break 值。
入门时按需求选择即可:逐项处理集合用 for,每轮先判断条件用 while,在循环内部决定何时退出用 loop。
3.11 continue 跳过本轮余下的动作 #
完整程序:
fn main() {
for score in [50, 60, 85, 100, 90] {
if score < 60 {
continue;
}
println!("及格成绩:{score}");
if score == 100 {
break;
}
}
}输出 60、85、100。
处理 50 时执行 continue,直接进入下一轮,不会打印。打印 100 后执行 break,整个循环结束,后面的 90 也不会打印。
continue 是“本轮剩下的不做了”,break 是“这个循环后面的轮次都不做了”。在普通筛选中,一个 if 往往已经足够;本例用来观察这两个关键字的不同效果。
3.12 完成三天学习统计程序 #
把函数、数组、循环、判断和输出放在一起,就能完成这个统计程序。下面的完整代码可以替换自己的 src/main.rs:
fn total_minutes(days: [u32; 3]) -> u32 {
let mut total = 0;
for minutes in days {
total += minutes;
}
total
}
fn reached_target(total: u32, target: u32) -> bool {
total >= target
}
fn main() {
let days: [u32; 3] = [20, 30, 40];
let target: u32 = 90;
let total = total_minutes(days);
let message = if reached_target(total, target) {
"已达标"
} else {
"未达标"
};
println!("三天总计:{total} 分钟");
println!("目标:{target} 分钟,{message}");
for minutes in days {
println!("单日记录:{minutes} 分钟");
}
}预期输出:
三天总计:90 分钟
目标:90 分钟,已达标
单日记录:20 分钟
单日记录:30 分钟
单日记录:40 分钟total_minutes(days: [u32; 3]) -> u32 明确要求输入三个 u32。reached_target(total: u32, target: u32) -> bool 负责比较,输出展示由 main 决定。
这里固定为三天,函数签名就只需使用第二节学过的数组类型。第五节会改用切片,接收任意长度的记录,现在先不用处理那部分语法。
最后的循环直接打印每条记录。函数内的 total、参数 total 和 main 中的 total 各有自己的局部范围。名称相同,也不能把它们当成同一个变量跨函数直接修改。
下载包中对应 basics/examples/ch03_flow.rs。在解压后的 rust-course 目录运行:
cargo run -p rust-basics --example ch03_flow先把目标改成 100,确认总计不变而判断变成“未达标”。再修改 days 中的一天,观察总计与明细一起变化。
本例的固定长度整数数组可以复制,因此传给 total_minutes 后,main 仍能遍历原来的 days。第四节会解释这类值的复制规则,以及为什么不能把它套用到所有类型。
语法细读:表达式、分号与控制流的类型 #
本章语法细读里的每个 Rust 程序可以分别替换自己项目的 src/main.rs:正常例子运行 cargo run,故意失败的例子运行 cargo check。下载包另保存为独立文件,直接编译的方法见下载包里的 syntax/README.md。各个程序相互独立。
读这一节时,把两个问题分开:一段代码会执行什么,以及它作为表达式会产生什么类型的值。能运行的语句,不一定能放进需要整数的返回位置。
块有类型,空结果也有类型 #
完整程序,下载包中为 syntax/syntax03-unit.rs:
fn plus_one(base: i32) -> i32 {
base + 1
}
fn procedure() {
println!("函数体已执行");
}
fn main() {
let value = {
let base = 4;
base + 1
};
let no_value = {
plus_one(4);
};
let result: () = procedure();
assert_eq!(no_value, ());
assert_eq!(result, ());
println!("块的值:{value},丢弃后:{no_value:?}");
}预期输出:
函数体已执行
块的值:5,丢弃后:(){ let base = 4; base + 1 } 是块表达式。let 声明没有提供末尾结果,最后的 base + 1 提供整数 5。{ plus_one(4); } 中的分号把函数调用的结果丢弃;没有尾表达式的普通块产生单元值 ()。
() 既是单元类型的写法,也是这个类型唯一的值,用来明确表示“没有额外结果”。省略函数的返回类型相当于返回 ()。因此只执行动作的函数调用也有确定的结果类型,能把结果赋给 let result: (),只是在普通代码里通常直接调用即可。
不要把“所有分号都产生 ()”当成规则。let 声明、表达式语句和函数体的尾表达式要分别辨认;带 return 的路径还可能根本不会走到块的结尾。
分支要提供同一结果类型,return 会结束当前函数 #
完整程序,下载包中为 syntax/syntax03-branches.rs:
fn choose(valid: bool) -> i32 {
if valid {
10
} else {
return 0;
}
}
fn main() {
println!("{} / {}", choose(true), choose(false));
let mut count = 0;
assert_eq!(count, 0);
let assignment = count = 3;
assert_eq!(assignment, ());
println!("赋值后的数:{count},赋值表达式:{assignment:?}");
}预期输出:
10 / 0
赋值后的数:3,赋值表达式:()if valid 的正常分支产生 i32。另一分支执行 return 0,直接结束函数,不会继续向外层 if 提供一个普通值。return 这类不会继续执行当前位置后续代码的表达式,其类型称为 never,写作 !。这里,走到函数末尾的分支交出整数,另一分支已经提前返回整数,因此两条路径都能满足函数的返回要求。
赋值表达式 count = 3 的结果是 (),不是被赋进去的 3。let assignment = count = 3; 只是用来观察这条规则;日常写法把赋值单独放一行更清楚。
以下完整程序故意无法编译,下载包中为 compile-fail/syntax03-branch-types.rs:
fn main() {
let value = if true { 10 } else { "十" };
println!("{value}");
}预期诊断包含 E0308。
即使条件文字写成 true,编译器也要检查另一个分支的类型。整数和文本不能在这里作为同一个 if 的结果。
以下完整程序故意无法编译,下载包中为 compile-fail/syntax03-condition.rs:
fn main() {
if 1 {
println!("进入分支");
}
}预期诊断包含 E0308。
if 的条件要求 bool。Rust 没有把整数 0 自动当成假、非零自动当成真的这条隐式转换;需要明确写 number != 0 等比较。
loop 可以带结果,标签决定退出哪一层 #
完整程序,下载包中为 syntax/syntax03-loop-value.rs:
fn main() {
let found = 'search: loop {
for number in 1..=3 {
if number == 2 {
break 'search number;
}
}
};
println!("找到:{found}");
}预期输出:
找到:2'search 是循环标签,名字前有单引号;它指定 break 的目标,不是生命周期标注。break 'search number 从外层 loop 退出并交出 number,所以整个 loop 表达式可以初始化 found。若有多个带值的退出分支,它们也需要提供兼容的结果类型。
以下完整程序故意无法编译,下载包中为 compile-fail/syntax03-while-value.rs:
fn main() {
while true {
break 2;
}
}预期诊断包含 E0571。
while 和 for 循环不能像这个 loop 一样通过 break value 返回结果。先区分三种循环,再判断 break 后能不能带值。
&& 和 || 不保证左右两侧都会执行 #
完整程序,下载包中为 syntax/syntax03-short-circuit.rs:
fn right() -> bool {
println!("计算右侧");
true
}
fn main() {
let a = false && right();
let b = true || right();
let c = true && right();
println!("{a} / {b} / {c}");
}预期输出:
计算右侧
false / true / truefalse && ... 已经能确定结果是假,true || ... 已经能确定结果是真,因此前两处没有调用 right。第三处需要右侧,才打印一次“计算右侧”。这种按需要计算的方式称为短路;把检查、打印或修改放到右侧之前,要确认是否允许跳过它。
自查:把第一个例子的 base + 1 后加分号,预测 value 的类型,再解释为什么原来的格式化输出也会报错。提示:单元值能用调试格式打印,却不支持原例给整数使用的普通显示格式。再把 choose 的 return 0 改成 println!("无效"),解释为什么原来的返回类型不能继续成立。
3.13 练习一:温度转换函数 #
写 celsius_to_fahrenheit(celsius: f64) -> f64,公式是 华氏温度 = 摄氏温度 × 1.8 + 32.0。在 main 中调用三次,输入 0、100、-40,分别输出 32、212、-40。
参考答案:先自己完成再展开
fn celsius_to_fahrenheit(celsius: f64) -> f64 {
celsius * 1.8 + 32.0
}
fn main() {
println!("{}", celsius_to_fahrenheit(0.0));
println!("{}", celsius_to_fahrenheit(100.0));
println!("{}", celsius_to_fahrenheit(-40.0));
}函数只负责转换,调用方用三个 println! 展示结果。第八节还会用这个公式学习模块和测试。
3.14 练习二:统计及格人数 #
写 count_passed(scores: [u32; 3]) -> u32。接收三个成绩,达到 60 算及格,返回及格人数。使用循环和 if,不需要调用还没有学过的方法。
至少检查三个输入:[59, 60, 90] 得到 2,[0, 59, 58] 得到 0,[60, 60, 60] 得到 3。
参考答案:先自己完成再展开
fn count_passed(scores: [u32; 3]) -> u32 {
let mut passed = 0;
for score in scores {
if score >= 60 {
passed += 1;
}
}
passed
}
fn main() {
assert_eq!(count_passed([59, 60, 90]), 2);
assert_eq!(count_passed([0, 59, 58]), 0);
assert_eq!(count_passed([60, 60, 60]), 3);
println!("练习通过");
}assert_eq! 检查左右两项是否相等,不相等时程序会失败。这里用它检查三个输入的结果,第八节再把这样的检查写成正式测试。
在下载包中运行 cargo run -p rust-basics --example answer03,应输出 练习通过。把判断错误地改成 score > 60 再运行,至少一个断言应失败;确认之后改回来。
3.15 进入第四节前检查 #
合上参考答案,从空白 main.rs 写一个程序:函数接收三个学习分钟数,返回总和;调用方判断是否达到目标,再用循环打印 1~3 天的序号。试着修改目标,预测输出,再运行核对。能做到这些,就完成了前三节的主要目标。
同时确认自己能回答:函数定义和调用有什么区别?返回表达式后面加分号为什么会报错?1..3 与 1..=3 相差什么?什么时候使用 break,什么时候使用 continue?
第四节将用可以增长的字符串和列表,说明函数之间怎样传递和借用数据。
官方延伸阅读与验证 #
文档查阅与示例复核日期:2026-10-13。正文完整程序使用 macOS arm64、Rust 1.99.0、Cargo 1.99.0 验证;故意增加返回表达式分号的例子按 E0308 检查。
一起讨论
选中正文中的文字,可以针对这段内容发起讨论。
正在读取讨论…
在 GitHub 查看与管理讨论 ↗