函数是 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 threadimpl 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提供运行时借用检查的内部可变性