Rust: Rust 函数与作用域:定义、传参、返回值与变量遮蔽
最后更新:2026-08-26
Rust 的函数是第一等公民——它的参数和返回值类型,是编译器为你把关的第一道防线。
函数是组织代码的基本单元。Rust 的函数语法和 C 系语言相似,但在参数模式匹配和表达式返回值上有着独特风格。
1. 你将学到
- 用
fn定义函数,指定参数和返回类型 - 参数的三种传递:传值、传引用、可变引用
- 函数的返回值与提前返回
- 块作用域与变量遮蔽
- 函数作为代码组织的基本单位
- 函数指针与高阶函数入门
2. 一个咖啡店的故事
(1) 痛苦:做咖啡没有标准化流程
Lisa 开了一家咖啡店,生意越来越好,但问题也来了:
- 每个店员做拿铁的方式各不相同
- 有的先加奶再加咖啡,有的先加咖啡再加奶
- 顾客投诉说"同一杯拿铁,每次味道都不一样"
- Lisa 想写一个标准化配方——就像函数一样:输入牛奶量和咖啡量,输出一杯固定风味的拿铁
"如果做咖啡能像调用函数一样稳定就好了:同样的输入,永远得到同样的输出。"
(2) Rust 函数的方案
RUST
// 做拿铁的"函数":输入参数,输出咖啡
fn make_latte(milk_ml: i32, coffee_ml: i32) -> String {
let result = format!("拿铁完成:{}ml 牛奶 + {}ml 咖啡,香浓可口!", milk_ml, coffee_ml);
result // 没有分号——表达式返回值
}
fn main() {
let my_coffee = make_latte(200, 30);
println!("{}", my_coffee);
let another = make_latte(150, 45);
println!("{}", another);
}
函数就像咖啡配方:参数是原料(牛奶量、咖啡量),返回值是成品。同样的输入保证同样的输出——这就是函数的确定性。
3. 函数定义
(1) 基本语法
RUST
fn function_name(param1: Type1, param2: Type2) -> ReturnType {
// 函数体
return_value // 表达式返回(不加分号)
}
graph TB
A[fn 函数名] --> B[(参数列表)]
A --> C[-> 返回类型]
A --> D[{ 函数体 }]
B --> E[每个参数: name: Type]
D --> F[最后表达式 = 返回值]
D --> G[return 关键字 = 提前返回]
(2) 参数与返回值的各种形式
| 形式 | 语法 | 示例 |
|---|---|---|
| 无参数无返回 | fn foo() |
fn greet() { println!("hi"); } |
| 有参数无返回 | fn foo(x: i32) |
fn show(n: i32) { println!("{}", n); } |
| 有参数有返回 | fn foo(x: i32) -> i32 |
fn double(x: i32) -> i32 { x * 2 } |
| 多个返回值(元组) | fn foo() -> (i32, bool) |
fn stats() -> (i32, bool) { (42, true) } |
| 提前返回 | return value; |
if x < 0 { return 0; } |
核心规则:函数体最后一个表达式就是返回值(不加分号)。用
return关键字做提前返回。
(3) 参数传递方式
| 传递方式 | 语法 | 所有权变化 | 适用场景 |
|---|---|---|---|
| 传值 | fn foo(s: String) |
所有权移入函数 | 函数需要消费该值 |
| 传不可变引用 | fn foo(s: &String) |
借用,所有权不变 | 只需读取数据 |
| 传可变引用 | fn foo(s: &mut String) |
借用,所有权不变 | 需要修改数据 |
(4) 返回值方式对比
| 方式 | 语法 | 所有权变化 | 示例 |
|---|---|---|---|
| 表达式返回 | 末行不加分号 | 所有权转出 | fn f() -> String { s } |
| return 返回 | return value; |
所有权转出 | if err { return None; } |
| 元组多返回 | -> (T1, T2) |
多个所有权转出 | fn f() -> (i32, bool) |
| 输出参数 | &mut T |
不转移所有权 | fn fill(buf: &mut Vec<i32>) |
4. 作用域与遮蔽
(1) 块作用域
Rust 的作用域由 {} 界定。每个块都有自己的作用域:
RUST
fn main() {
let x = 10; // 外层作用域
{
let y = 20; // 内层作用域,y 只在这里可用
println!("内部: x={}, y={}", x, y); // ✅ 可以访问外层 x
}
// println!("{}", y); // ❌ 编译错误:y 不在此作用域
println!("外部: x={}", x); // ✅
}
(2) 变量遮蔽(Shadowing)
外层变量可以被内层同名变量遮蔽,内层代码只能看到内层变量:
RUST
fn main() {
let x = 1; // 外层 x = 1
{
let x = 2; // 内层 x 遮蔽了外层 x
println!("内部 x = {}", x); // 输出: 内部 x = 2
}
println!("外部 x = {}", x); // 输出: 外部 x = 1
}
(3) 函数参数的作用域
函数参数在整个函数体内部可见:
RUST
fn foo(x: i32) {
// x 在整个函数体内部可用
let y = x + 1;
println!("x={}, y={}", x, y);
} // 函数结束,x 和 y 都被销毁
5. 函数示例
▶ 示例 1:各种函数签名(难度 ⭐)
RUST
// ============================================
// 不同参数和返回类型组合的函数
// ============================================
// 无参数无返回值
fn print_separator() {
println!("-------------------");
}
// 单参数有返回值
fn square(x: i32) -> i32 {
x * x // 不加分号,表达式返回
}
// 多参数
fn area(width: u32, height: u32) -> u32 {
width * height
}
// 返回多个值(元组)
fn min_max(a: i32, b: i32) -> (i32, i32) {
if a < b {
(a, b) // 元组作为返回值
} else {
(b, a)
}
}
fn main() {
print_separator();
println!("5 的平方: {}", square(5));
println!("面积 10x20: {}", area(10, 20));
let (min, max) = min_max(100, 50);
println!("min={}, max={}", min, max);
}
输出:
TEXT
📖 仅展示
-------------------
5 的平方: 25
面积 10x20: 200
min=50, max=100
min_max返回元组,调用时用let (min, max)解构赋值。这是 Rust 中"返回多个值"的标准做法。
▶ 示例 2:提前返回与守卫模式(难度 ⭐⭐)
RUST
// ============================================
// 提前返回:检查条件,不满足就退出
// ============================================
fn safe_divide(a: f64, b: f64) -> f64 {
if b == 0.0 {
println!("错误:除数不能为 0");
return f64::NAN; // 提前返回 NaN(Not a Number)
}
a / b // 正常情况,表达式返回
}
fn get_grade(score: i32) -> &'static str {
if score < 0 || score > 100 {
return "无效分数";
}
if score >= 90 { return "A"; }
if score >= 80 { return "B"; }
if score >= 70 { return "C"; }
if score >= 60 { return "D"; }
"F"
}
fn main() {
println!("10 / 3 = {}", safe_divide(10.0, 3.0));
println!("10 / 0 = {}", safe_divide(10.0, 0.0));
println!("分数 85: {}", get_grade(85));
println!("分数 -5: {}", get_grade(-5));
}
输出:
TEXT
📖 仅展示
10 / 3 = 3.3333333333333335
错误:除数不能为 0
10 / 0 = NaN
分数 85: B
分数 -5: 无效分数
提前返回(
return)常用于"先检查后执行"模式。把错误条件和边界情况在最前面处理掉,后面就是"安全区"的顺利逻辑。
▶ 示例 3:块表达式与作用域测试(难度 ⭐⭐)
RUST
// ============================================
// 块表达式的作用域和返回值
// ============================================
fn main() {
let outer = "外层变量".to_string();
let inner_result = {
let inner = "内层变量".to_string();
println!("内部可以访问: {}", outer); // ✅ 内层可以访问外层
// 遮蔽同名变量
let outer = 42;
println!("遮蔽后的 outer: {}", outer); // 42,不是字符串
inner.len() // 块表达式返回值:inner 的长度
}; // inner 在此处被销毁
println!("块返回的值: {}", inner_result);
println!("外部 outer: {}", outer); // ✅ 外层 outer 还在
// println!("{}", inner); // ❌ 编译错误:inner 不在此作用域
}
输出:
TEXT
📖 仅展示
内部可以访问: 外层变量
遮蔽后的 outer: 42
块返回的值: 8
外部 outer: 外层变量
块作用域规则:内层可以访问外层变量,外层不能访问内层变量。遮蔽只是暂时"屏蔽"了外层变量,离开内层块后外层变量恢复正常。
▶ 示例 4:函数指针与高阶函数(难度 ⭐⭐⭐)
RUST
// ============================================
// 函数作为参数传递——高阶函数入门
// ============================================
type MathOp = fn(i32, i32) -> i32;
fn add(a: i32, b: i32) -> i32 { a + b }
fn sub(a: i32, b: i32) -> i32 { a - b }
fn mul(a: i32, b: i32) -> i32 { a * b }
fn calculate(op: MathOp, a: i32, b: i32) -> i32 {
op(a, b)
}
fn apply_twice(f: fn(i32) -> i32, x: i32) -> i32 {
f(f(x))
}
fn main() {
let result1 = calculate(add, 10, 5);
let result2 = calculate(sub, 10, 5);
let result3 = calculate(mul, 10, 5);
println!("10 + 5 = {}", result1);
println!("10 - 5 = {}", result2);
println!("10 * 5 = {}", result3);
let double = |x: i32| x * 2;
let result4 = apply_twice(double, 3);
println!("double(double(3)) = {}", result4);
let increment = |x: i32| x + 1;
let result5 = apply_twice(increment, 5);
println!("increment(increment(5)) = {}", result5);
let alice_points = 80;
let bonus = |base: i32| -> i32 { base + 10 };
println!("Alice 加分后: {}", bonus(alice_points));
}
输出:
TEXT
📖 仅展示
10 + 5 = 15
10 - 5 = 5
10 * 5 = 50
double(double(3)) = 12
increment(increment(5)) = 7
Alice 加分后: 90
函数指针(
fn类型)和闭包(|x| x * 2)都可以作为参数传递给高阶函数。函数指针指向编译时确定的代码地址,闭包可以捕获环境变量。
▶ 示例 5:综合练习——学生成绩处理(难度 ⭐⭐⭐)
RUST
// ============================================
// 综合示例:函数、作用域和遮蔽的组合运用
// ============================================
fn average(scores: &[i32]) -> f64 {
if scores.is_empty() {
return 0.0;
}
let sum: i32 = scores.iter().sum();
sum as f64 / scores.len() as f64
}
fn classify(score: i32) -> &'static str {
match score {
90..=100 => "A",
80..=89 => "B",
70..=79 => "C",
60..=69 => "D",
_ => "F",
}
}
fn report(names: &[&str], scores: &[i32]) {
let avg = average(scores);
println!("班级平均分: {:.1}", avg);
for (i, name) in names.iter().enumerate() {
let score = scores[i];
let grade = classify(score);
let status = if score >= 60 { "通过" } else { "未通过" };
println!("{}: {} 分, 等级 {}, {}", name, score, grade, status);
}
let passing = scores.iter().filter(|&&s| s >= 60).count();
println!("通过人数: {}/{}", passing, scores.len());
}
fn main() {
let names = ["Alice", "Bob", "Charlie", "David"];
let scores = [95, 72, 58, 88];
println!("=== 成绩报告 ===");
report(&names, &scores);
println!("\n--- 作用域演示 ---");
let result = {
let scores = [100, 90, 80];
let avg = average(&scores);
format!("前三名平均: {:.1}", avg)
};
println!("{}", result);
let avg = "外层 avg";
println!("外层变量: {}", avg);
}
输出:
TEXT
📖 仅展示
=== 成绩报告 ===
班级平均分: 78.2
Alice: 95 分, 等级 A, 通过
Bob: 72 分, 等级 C, 通过
Charlie: 58 分, 等级 F, 未通过
David: 88 分, 等级 B, 通过
通过人数: 3/4
--- 作用域演示 ---
前三名平均: 90.0
外层变量: 外层 avg
此示例综合运用了函数定义、切片参数、match 分类、迭代器方法和块作用域遮蔽。函数参数用
&[T]切片最灵活,块作用域内遮蔽同名变量不影响外层。
❓ 常见问题
Q 最后一行表达式不加分号作为返回值,容易忘加分号怎么办?
A 忘了加分号编译器会报错,提示你应该是语句还是表达式。
Q return 和表达式返回有什么区别?
A return 用于提前返回,表达式返回用于函数末尾。
Q 函数参数为什么必须写类型?
A 因为 Rust 不会推断函数参数的类型。
Q 遮蔽(shadowing)是不是不好的习惯?
A 适度遮蔽是好的。
Q 函数可以嵌套吗?
A 可以,Rust 支持函数内部定义函数。
📖 小节
- 函数用
fn关键字定义,参数必须标注类型 - 函数体最后一个表达式不加分号即为返回值
return关键字用于提前返回{}块定义作用域,内层可访问外层变量,反之不行- 变量遮蔽让内层可以声明同名变量暂时覆盖外层变量
- 函数参数在整个函数体内部作用域可见
📝 作业
- 难度 ⭐:写一个函数
fn is_even(n: i32) -> bool,判断整数是否为偶数,然后写main调用它打印 1 到 10 每个数的奇偶性。 - 难度 ⭐⭐:写一个函数
fn max_of_three(a: i32, b: i32, c: i32) -> i32,返回三个数的最大值。(不允许用标准库的.max(),自己写比较逻辑。) - 难度 ⭐⭐⭐:写一个函数
fn calculator(op: char, a: f64, b: f64) -> f64,根据op参数('+''-''*''/')做对应运算,除数为 0 时返回f64::NAN。在main中测试这 4 种运算。