4. 函数、参数传递与返回值设计
三种函数签名与所有权影响
本案例围绕一个“文本统计”小任务展开:统计字符串的字符数,并返回其中第一个单词的引用。通过三种签名对比值传递、引用传递和返回引用的差异,再用生命周期标注让返回引用通过编译。
- 值传递:
fn take_owned(s: String) -> usize,调用后实参被 move,调用方不能再使用原变量。 - 引用传递:
fn count_chars(s: &str) -> usize,只借用,调用后原变量仍可用。 - 返回引用:
fn first_word(s: &str) -> &str,返回的引用与输入引用绑定同一生命周期,调用方需保证被引用数据存活。
完整可运行代码
下面代码把三种签名放在同一个程序中,并演示调用方在每种情况下的可用性。注意 first_word 的返回引用与输入引用共享生命周期 'a,这是编译器接受它的关键。
fn take_owned(s: String) -> usize {
s.chars().count()
}
fn count_chars(s: &str) -> usize {
s.chars().count()
}
fn first_word<'a>(s: &'a str) -> &'a str {
match s.find(' ') {
Some(i) => &s[..i],
None => s,
}
}
fn main() {
// ① 值传递:s1 被 move
let s1 = String::from("hello rust");
let n1 = take_owned(s1);
println!("take_owned 返回: {}", n1);
// println!("{}", s1); // 取消注释会编译错误:value borrowed here after move
// ② 引用传递:s2 仍可用
let s2 = String::from("hello rust");
let n2 = count_chars(&s2);
println!("count_chars 返回: {}, s2 仍可用: {}", n2, s2);
// ③ 返回引用:借用 s2,返回切片
let w = first_word(&s2);
println!("first_word 返回: {}", w);
// s2 在 w 使用期间保持不可变借用,不能同时可变借用
}
生命周期标注为什么必要
当函数返回引用时,编译器无法自动判断返回的引用来自哪个输入参数。若签名写成 fn first_word(s: &str) -> &str,编译器会报 missing lifetime specifier。加上 <'a> 后,明确告诉编译器:返回的引用与输入 s 的生命周期相同,调用方必须保证 s 在返回引用使用期间一直存活。
常见错误:返回局部变量的引用。例如
fn bad() -> &str { let s = String::from("x"); &s } 会报 returns a value referencing data owned by the current function,因为 s 在函数结束时被释放。签名选择速查
| 签名形式 | 所有权变化 | 调用方后续可用性 | 典型场景 |
|---|---|---|---|
fn f(s: String) | move 进函数 | 原变量失效 | 消费数据、转换后不再需要原值 |
fn f(s: &str) | 只读借用 | 原变量仍可用 | 统计、校验、格式化 |
fn f(s: &mut String) | 可变借用 | 借用结束后可用 | 原地修改、追加内容 |
fn f<'a>(s: &'a str) -> &'a str | 借用并返回借用 | 原变量在返回引用使用期内被借用 | 切片、解析、零拷贝提取 |
验证方法:把
first_word 的 <'a> 去掉,观察编译器报错;再把 take_owned(s1) 后对 s1 的打印取消注释,观察 move 错误。两次报错分别对应“生命周期缺失”和“所有权已转移”,是本案例最直接的验证点。