【Rust】 4. 函数与闭包
第一部分:函数 (Functions)
- 函数简介
函数是 Rust 中封装可重用代码的基本构建块,使用 fn 关键字定义。
定义语法:
fn function_name(parameter1: Type1, parameter2: Type2, ...) -> ReturnType {// 函数体// 可以包含多条语句和表达式// 可以使用 return 语句返回值
}
-
fn:定义函数的关键字。
-
function_name:函数名,遵循 snake_case 命名规范(全小写,下划线分隔)。
-
parameter: Type:参数列表,每个参数都必须显式注明类型。
-
-> ReturnType:指定函数的返回类型。
-
函数体:由 {} 包围,包含实际的执行逻辑。
核心特性:
-
函数返回值可以通过 return 语句显式指定,也可以通过省略最后一个表达式末尾的分号来隐式返回。
-
若未指定返回类型,编译器默认返回单元类型 ()。
-
程序入口是 fn main()。
-
Rust 不关心函数定义的位置(可在 main 之前或之后),只要在作用域内定义了即可调用。
-
函数体内可以定义其他项(如静态变量、常量、嵌套函数、trait、类型等)。
- 函数实例
fn main() {let num = add(1, 2); // 调用函数println!("{}", num) // 输出: 3
}fn add(x: i32, y: i32) -> i32 {x + y // 这是一个表达式,其值被隐式返回
}
- 参数与返回值
-
参数:函数可以接受零个或多个强类型参数。
-
返回值:函数可以返回一个值,或不返回任何值(即返回 ())。
-
语句 vs 表达式:这是理解返回值的核心。
- 语句:以分号 ; 结尾,执行操作但不返回值(类型为 ())。
- 表达式:不以分号结尾,会计算并产生一个值。
错误示例:
fn add(x: i32, y: i32) -> i32 {x + y; // 错误!加了分号,这变成了语句,返回的是 (),与声明的返回类型 i32 冲突
}
// 正确做法是去掉分号:`x + y`
- 参数传递方式
Rust 支持两种参数传递方式:
-
按值传递 (Pass by Value)
-
函数获得参数的副本。
-
在函数内部修改副本不会影响原始值。
-
适用于实现 Copy trait 的类型(如基本整数、布尔、字符等)。
-
-
按引用传递 (Pass by Reference)
-
函数获得参数的引用(指针),而非值本身。
-
不可变引用 (&T):允许函数读取但不能修改原始值。
-
可变引用 (&mut T):允许函数读取和修改原始值。
-
适用于所有类型,尤其是不想转移所有权或数据较大时。
-
示例:
fn increment_by_value(mut num: i32) { // 按值传递,获取副本num += 1; // 修改副本println!("Result: {}", result); // 输出: 6
}fn increment_by_reference(num: &mut i32) { // 按可变引用传递*num += 1; // 解引用并修改原始值
}fn main() {let mut value = 5;increment_by_value(value); // 传递值的副本println!("Original value: {}", value); // 输出: 5 (原始值未变)increment_by_reference(&mut value); // 传递可变引用println!("Updated value: {}", value); // 输出: 6 (原始值被修改)
}
- 函数作为值与高阶函数 (Higher-Order Functions)
在 Rust 中,函数是一等公民(First-class citizens),可以像普通值一样被赋值、传递和返回。
-
函数指针类型:fn(i32, i32) -> i32
-
高阶函数:指那些以函数为参数和/或返回函数的函数。
示例:
fn add(a: i32, b: i32) -> i32 { a + b }
fn subtract(a: i32, b: i32) -> i32 { a - b }// 高阶函数:接受一个函数指针 `operation` 作为参数
fn calculate(operation: fn(i32, i32) -> i32, a: i32, b: i32) -> i32 {operation(a, b) // 调用传入的函数
}fn main() {let result1 = calculate(add, 5, 3); // 传递函数 `add`println!("Result1: {}", result1); // 输出: 8let result2 = calculate(subtract, 8, 4); // 传递函数 `subtract`println!("Result2: {}", result2); // 输出: 4
}
- 泛型函数 (Generic Functions)
泛型函数允许编写适用于多种类型的通用代码,使用类型参数(通常用 表示)实现。
示例:
use std::fmt::Display;// <T: Display> 表示类型 T 必须实现 Display trait
fn print_type<T: Display>(value: T) {println!("Type: {}", std::any::type_name::<T>()); // 打印类型名println!("Value: {}", value); // 打印值
}fn main() {print_type(5); // T 是 i32print_type("Hello"); // T 是 &str
}
// 输出:
// Type: i32
// Value: 5
// Type: &str
// Value: Hello
- 发散函数 (Diverging Functions)
发散函数永远不会返回到调用者。其返回类型标记为 !(never type)。
常见场景:
-
panic! 宏及其变体(如 unimplemented!, unreachable!)。
-
无限循环 loop {}。
-
进程退出函数(如 std::process::exit)。
示例:
fn diverges() -> ! {panic!("This function will never return!");
}
特点: 发散表达式可以被转换为任何类型,这在需要统一类型的场合(如 match 表达式的不同分支)很有用。
8. 常量函数 (const fn)
使用 const 关键字修饰的函数可以在编译时被求值,其结果可作为常量使用。
-
限制:const fn 的函数体内只能使用有限的编译时可确定的操作。
-
状态:此功能仍在发展和完善中。
示例:
const fn square(x: i32) -> i32 {x * x
}const CONST_SQUARE: i32 = square(5); // 编译时计算
第二部分:闭包 (Closures)
- 闭包简介
闭包是匿名函数,可以捕获其定义所在作用域中的变量。它们非常灵活,可以像普通值一样传递和存储。
2. 基本语法与演变
闭包通常定义为 |args| expression 的形式。
演变过程:
// 1. 标准函数
fn add_one(x: i32) -> i32 { x + 1 }// 2. 闭包 (完整写法)
let add_one_v1 = |x: i32| -> i32 { x + 1 };// 3. 闭包 (省略返回类型,编译器推断)
let add_one_v2 = |x: i32| { x + 1 };// 4. 闭包 (省略大括号,单表达式)
let add_one_v3 = |x| x + 1; // 类型也可由调用处推断// 使用
println!("{}", add_one_v3(5)); // 输出: 6
- 捕获环境变量
这是闭包与函数最核心的区别:闭包可以捕获并使用其定义作用域内的变量。
闭包可以:
fn main() {let x = 10;let add = |y| x + y; // 捕获了外部变量 `x`println!("{}", add(5)); // 输出: 15
}
函数不可以:
fn main() {let x = 10;fn add(y: i32) -> i32 {x + y // 错误!函数无法捕获环境变量 `x`}
}
- 捕获方式与闭包 Trait
Rust 闭包根据其如何使用捕获的变量,会自动实现以下三种 Trait 之一:
Trait | 捕获方式 | 是否可变 | 描述 | 示例 |
---|---|---|---|---|
Fn | 不可变借用(&T) | 不可变 | 只读取捕获变量的值,不修改它们。最常见。 | let c = |x| x + captured_var; |
FnMut | 可变借用(&mut T) | 可变 | 可以修改捕获的变量。 | let mut c = |x { *captured_var += x; }; |
FnOnce | 获取所有权(T) | 消耗 | 会消耗(移动)捕获的变量,只能调用一次。 | let c = |x| { drop(captured_var); }; |
- move 关键字:强制闭包通过值(获取所有权)来捕获变量,通常用于将闭包传递到新线程时避免生命周期问题。
let s = String::from("hello");let f = move || println!("{}", s); // `s` 被移动进闭包,所有权不再在主函数中// println!("{}", s); // 错误!s 的所有权已移入闭包f();
- 编译器会自动为闭包选择最合适的 Trait,实现静态分发。
- 闭包 vs 函数
项目 | 函数 | 闭包 |
---|---|---|
是否有名字 | 有 (fn 定义) | 无(匿名) |
捕获外部变量 | ❌ 不能 | ✅ 可以 |
类型 | 函数指针 (fn()) | 匿名结构体,实现 Fn/FnMut/FnOnce |
类型标注 | 必须显式注明参数和返回类型 | 通常可省略,由编译器推断 |
存储 | 代码段 | 通常存储在栈上(可能捕获数据) |
总结: 闭包提供了比函数更大的灵活性,特别是在需要捕获上下文和进行函数式编程的场景中。函数则更简单、更明确,适用于不需要捕获环境的通用逻辑。