函数是 Rust 程序的基本构建块,而函数签名就像是一份详细的说明书,它告诉我们这个函数期望接收什么样的参数,会返回什么类型的结果,以及可能出现哪些错误情况。
当我们调用一个函数时,函数签名就是我们与这个函数之间的“契约”。通过查看函数签名,我们就能清楚地知道:
Rust 的类型系统让函数设计变得既安全又灵活。通过 Result 和 Option 类型,我们可以优雅地处理可能失败的操作和可能为空的值。通过泛型和 trait 约束,
我们可以编写适用于多种类型的通用函数。通过 where 子句,我们可以为复杂的泛型函数添加清晰的类型约束。

Rust 函数的返回值有着独特而优雅的设计。与许多语言不同,Rust 函数的返回值由函数体中最后一个表达式决定,而不是通过 return 关键字(当然,我们也可以使用 return 提前返回)。
在 Rust 中,表达式会产生值,而语句则执行操作但不产生值。函数体中最后一个表达式(注意:没有分号)的值就是函数的返回值。
|fn area(width: u32, height: u32) -> u32 { width * height } fn print_area(w: u32, h: u32) { println!("{}", area(w, h)); } fn main() { print_area(6, 7); }
|42
在实际编程中,我们经常需要在函数执行过程中根据条件提前返回。Rust 提供了多种控制流工具,让我们能够以简洁明确的方式处理这些情况。
使用 return 关键字可以在任何时候从函数中返回值。这在需要根据条件提前退出函数时特别有用:
|fn divide(a: i32, b: i32) -> Option<i32> { if b == 0 { return None; } Some(a / b) }
Option 与 Result:不确定性的类型化在 Rust 里,我们不再把“不存在”或“出错”藏在一个神秘的空指针或哨兵值里,而是把它们变成了显式的类型:
Option<T> 表示“也许有一个 T,也许没有”。用 Some(T) 和 None 两种变体表达“存在/不存在”。Result<T, E> 表示“要么得到一个 T,要么得到一个错误 E”。用 Ok(T) 和 Err(E) 两种变体表达“成功/失败”。这两个类型把不确定性从“运行时报错”前移到了“编译期必须处理”,从而让调用者在代码层面明确地思考:如果没有值怎么办?如果失败了怎么办?
通俗地说:
Option<T>。例如:在切片里找第一个偶数,可能找不到,这并不是错误。Result<T, E>。例如:解析字符串成数字,失败需要携带错误信息或错误类型。|fn first_even(nums: &[i32]) -> Option<i32> { nums.iter().cloned().find(|n| n % 2 == 0) } fn parse_and_double(s: &str) -> Result<
|Some(4) Ok(42)
? 运算符:把“检查并返回”写成一行? 的读法是:“如果这里是错误,就立刻把错误往上传;否则把里面的值拿出来继续执行”。
它可以用在返回 Result 的函数里,也可以配合 Option 使用(此时函数返回类型也必须是 Option)。
|fn half_strict(s: &str) -> Result<i32, String> { let n: i32 = s.parse().map_err(|_| format!("bad int: {s}"))?; // 解析失败就立刻向上返回错误 if n % 2 != 0 { return
|Ok(20) Some("hello")
很多时候我们一开始只有一个 Option<T>(有或没有),但接下来需要把它“提升”为 Result<T, E>,以便携带错误信息。反过来,有时我们希望把 Result<T, E> 的错误抛弃,只关心“有没有值”。
Option<T>::ok_or(err) / ok_or_else(|| err):把 Option 变成 Result。Result<T, E>::ok():把 Result 变成 Option,丢掉错误。Option<Result<T, E>>::transpose():把“Option 里的 Result”翻转为“Result 里的 Option”。|fn first_nonempty(words: &[&str]) -> Result<&str, &'static str> { words.iter().copied().find(|w| !w.is_empty()).ok_or("no word") }
泛型编程的核心思想是"对能力编程,而非对类型编程"。我们通过泛型参数和约束(trait bounds)来描述函数需要什么样的"能力",而不是限定具体的类型。这种设计哲学让函数能够处理多种不同的类型,只要这些类型满足我们声明的能力要求。
使用 where 子句可以让约束声明更加清晰,特别是在有多个泛型参数或复杂约束时。这样写出的函数不仅在编译时保证类型安全,还能为不同的输入类型生成优化的代码,实现零成本抽象。
比如下面的例子中,我们不关心 T 具体是什么类型,只要求它能够被显示(实现了 Display trait)。这样无论是数字、字符串还是自定义类型,只要能打印,就能使用这个函数:
|use std::fmt::Display; fn join_as_string<T>(items: &[T], sep: &str) -> String where T: Display { let mut out = String::new(); for (i, item) in items.iter()
每个函数在 Rust 中都是一个"函数项"(function item),具有独特的零大小类型。函数项可以强制转换为函数指针类型 fn(T) -> U,这是一个指向函数的指针类型。
函数指针实现了所有三个闭包 trait(Fn、FnMut、FnOnce),这意味着任何接受闭包的地方都可以传入函数。
|fn apply_once<F: FnOnce(i32) -> i32>(f: F, x: i32) -> i32 { f(x) } fn add5(x: i32) -> i32 { x + 5 } fn main() { let r1 = apply_once(add5,
|15, 17
闭包的捕获方式直接影响其性能特征和使用场景。Rust 编译器会根据闭包内部如何使用捕获的变量,自动选择最合适的捕获方式:
不可变借用捕获(Fn):当闭包只需要读取外部变量时,会以不可变借用方式捕获。这种闭包可以被多次调用,因为它不会修改或消费捕获的值。这是最轻量级的捕获方式,运行时开销最小。
可变借用捕获(FnMut):当闭包需要修改外部变量时,会以可变借用方式捕获。这种闭包可以被多次调用,但每次调用时需要独占访问捕获的变量。
移动捕获(FnOnce):当闭包需要获取外部变量的所有权时,会移动捕获。这种闭包只能被调用一次,因为调用后会消费掉捕获的值。
在多线程中使用 move 强制闭包移动捕获外部变量的所有权。
|use std::thread; fn main() { let msg = String::from("hello"); // 使用 move,把 msg 的所有权移动进闭包,使其能够在线程中安全使用 let handle = thread::spawn(move || { println!("{msg} from thread"); }); // println!("{msg}"); // 编译错误:msg 已被移动到线程闭包中 handle.join().
|hello from thread
impl Trait 与返回抽象直觉上,impl Trait 就是“我不告诉你具体类型,但保证它实现了某个能力(trait)”。这在两个位置最常用:
impl Trait把“能被遍历”的东西统称为 IntoIterator<Item = i64>,无论它是 Vec<i64> 还是数组,都能被这一个签名接住:
|fn sum_all(nums: impl IntoIterator<Item = i64>) -> i64 { nums.into_iter().sum() } fn main() { println!("{}", sum_all(vec![1, 2, 3])); println!("{}"
|6 60
等价的“泛型版”写法(功能一致,语法更通用,可在多个位置复用相同类型参数):
|fn sum_all_generic<I>(nums: I) -> i64 where I: IntoIterator<Item = i64> { nums.into_iter().sum() }
impl Trait当我们返回一个迭代器流水线时,它的真实类型很长(多层适配器嵌套)。impl Iterator<Item = T> 可以把细节藏起来:
|fn evens_up_to(n: u32) -> impl Iterator<Item = u32> { (0..=n).filter(|x| x % 2 == 0) } fn main() { let v: Vec<_> = evens_up_to(10
|[0, 2, 4, 6, 8, 10]
返回 impl Trait 时,函数的所有返回分支必须具有相同的“具体”类型(虽然都实现了相同 trait)。下面这种“不同分支返回不同迭代器类型”的写法无法编译:
|// 不可编译示例:两个分支返回了不同的具体迭代器类型 // fn pick_iter(flag: bool) -> impl Iterator<Item = i32> { // let base = 0..5; // if flag { base.clone().map(|x| x + 1) } else { base.filter(|x| x % 2 == 0) } // }
解决方案 A:使用特征对象 Box<dyn Iterator<Item = i32>>,牺牲少量动态分发开销,换来“运行时选择分支”的灵活性:
|fn pick_iter(flag: bool) -> Box<dyn Iterator<Item = i32>> { let base = 0..5; if flag { Box::new(base.clone().map(|x| x + 1)) }
|[1, 2, 3, 4, 5]
解决方案 B:把两种分支“抬升”为一个枚举并为其实现 Iterator,获得静态分发与更好的优化(代码略)。
impl Trait当返回值里包含借用时,生命周期要从输入“透传”到输出(编译器据此保证返回的引用不悬垂):
|fn words<'a>(s: &'a str) -> impl Iterator<Item = &'a str> { s.split_whitespace() } fn main() { for w in words("hi rust") { print!("[{w}] "); } }
|[hi] [rust]
9. Option基础练习
实现函数last_char,安全地获取字符串的最后一个字符:
last_char(s: &str) -> Option<char>NoneSome(最后一个字符)chars().last()方法实现|fn last_char(s: &str) -> Option<char> { s.chars().last() } fn main() { println!("{:?}", last_char("rust")); // Some('t') println!("{:?}", last_char("你好")); // Some('好')
10. Result和?运算符练习
实现函数parse_and_half,解析字符串为整数,如果是偶数则返回其一半,如果是奇数则返回错误:
parse_and_half(s: &str) -> Result<i32, String>parse()解析字符串为整数,使用?运算符处理解析错误|fn parse_and_half(s: &str) -> Result<i32, String> { // 使用?运算符:解析失败时自动返回错误 let n: i32 = s.parse().map_err(|e| format!("解析失败: {}", e))?; // 检查是否为偶数 if n % 2
11. Option与Result转换练习
实现函数to_level,将Option<&str>转换为Result<u32, &'static str>:
to_level(opt: Option<&str>) -> Result<u32, &'static str>Option为None,返回错误信息"环境变量不存在"Option为Some(s),尝试将字符串解析为u32ok_or()、and_then()等方法实现|fn to_level(opt: Option<&str>) -> Result<u32, &'static str> { // 方法1:使用ok_or + and_then opt.ok_or("环境变量不存在") .and_then(|s| s.parse::<u32>().map_err(|
12. 泛型函数和trait约束练习
实现泛型函数max_of,返回两个值中的较大者:
max_of<T>(a: T, b: T) -> T where T: Ord + Copywhere子句声明约束:T必须实现Ord(可比较)和Copy(可复制)max()方法或if表达式比较两个值Copy,可以返回什么类型?|use std::cmp::max; // 方式1:要求Copy(可以返回T) fn max_of<T>(a: T, b: T) -> T where T: Ord + Copy { max(a, b) // 或者 if a > b { a } else { b } } // 方式2:不要求Copy,返回引用 fn max_of_ref<T>(a: &T
13. impl Trait和迭代器练习
实现函数avg,计算任意可迭代集合的平均值:
avg(nums: impl IntoIterator<Item = f64>) -> Option<f64>impl IntoIterator接受任何可迭代类型(Vec、数组、迭代器等)NoneSome(平均值)|fn avg(nums: impl IntoIterator<Item = f64>) -> Option<f64> { let mut sum = 0.0; let mut count = 0; for num in nums { sum += num; count += 1; }
14. 生命周期和字符串切片练习
实现函数prefix,安全地返回字符串的前n个字节:
prefix<'a>(s: &'a str, n: usize) -> &'a str'a确保返回的引用有效n.min(s.len())确保不越界|// 按字节数截断 fn prefix<'a>(s: &'a str, n: usize) -> &'a str { let end = n.min(s.len()); &s[..end] } // 按字符数截断(支持Unicode) fn prefix_chars<'a
15. 闭包和状态捕获练习
实现函数make_counter,返回一个闭包作为计数器:
make_counter(start: i32, step: i32) -> impl FnMut() -> i32stepstartmove关键字捕获变量RefCell或Cell)存储计数器值|use std::cell::Cell; fn make_counter(start: i32, step: i32) -> impl FnMut() -> i32 { let count = Cell::new(start); move || { let current = count.get(); count
|Some('t') Some('好') None Some('🦀')
说明:
chars()返回字符迭代器,支持Unicode字符last()返回迭代器的最后一个元素,返回Option<char>last()返回None|输入: 40, 结果: Ok(20) 输入: 41, 错误: Err("41 不是偶数") 输入: abc, 错误: Err("解析失败: invalid digit found in string") 输入: -10, 结果: Ok(-5) 输入: 0, 结果: Ok(0)
说明:
parse()返回Result<T, ParseIntError>?运算符在错误时自动返回,成功时提取值map_err()用于转换错误类型|Ok(3) Ok(42) Err("解析失败") Err("环境变量不存在")
说明:
ok_or()将Option<T>转换为Result<T, E>and_then()用于链式处理,如果前一步是Ok则继续,否则返回错误map_err()用于转换错误类型|max(10, 7) = 10 max(3.14, 2.71) = 3.14 max_ref(&10, &7) = 10 max_ref(&s1, &s2) = banana
说明:
Ord trait提供比较能力(<, >, <=, >=)Copy trait允许值被复制(不移动所有权)Copy,可以返回引用&T,避免移动所有权where子句使约束更清晰,特别是复杂约束时|Some(2.0) Some(20.0) None None 使用迭代器版本: Some(2.0) None
说明:
impl IntoIterator<Item = f64>接受任何可迭代类型?运算符处理空集合的情况fold()用于累积计算(总和和数量)impl Trait简化了函数签名,无需显式泛型参数|原字符串: "abcdef" 前3个字节: "abc" 前10个字节(越界): "abcdef" 原字符串: "你好Rust" 前3个字节: "你" 前2个字符: "你好" 前4个字符: "你好Ru"
说明:
'a表示返回的引用与输入引用有相同的生命周期n.min(s.len())确保不越界char_indices()找到字符边界|计数器1(初始10,步长2): 10 12 14 计数器2(初始0,步长5): 0 5 10 计数器3(初始100,步长-10): 100 90 80
说明:
impl FnMut() -> i32表示返回一个可变的闭包move关键字强制闭包获取捕获变量的所有权Cell用于在不可变上下文中修改值(内部可变性)RefCell提供运行时借用检查的内部可变性