11|生命周期:表达返回引用来自哪里
目标:为返回输入片段的函数写出正确的生命周期关系,并识别不能返回局部引用的情况。
TextLab 的统计结果只有整数,不需要引用输入继续存活。但如果增加“显示最长的一行”,为了避免复制文本,我们可能想返回原输入的一段 &str。此时函数签名必须让编译器知道输出引用依赖哪个输入。
生命周期标注描述引用有效期之间的关系,不会延长对象寿命,也不会产生运行时计时器。单输入引用的常见函数可以省略标注;有多个输入引用,而且输出可能来自任意一个时,通常需要明确关系。'a 是生命周期参数名,可以换成其他名字。
在两个借来的片段之间选择
创建 examples/lifetimes.rs:
fn longer<'a>(left: &'a str, right: &'a str) -> &'a str {
if left.len() >= right.len() {
left
} else {
right
}
}
fn main() {
let left = String::from("rust");
{
let right = String::from("ownership");
let chosen = longer(&left, &right);
println!("chosen={chosen}");
}
println!("left={left}");
}
这里的“longer”按字节长度比较,刻意使用 ASCII,避免把 Unicode 统计单位混进借用实验。'a 表示两个输入都必须在输出被使用的那段期间有效;调用时可以把较长的有效期缩短到共同可用的范围,并不是要求两个原始变量同时创建、同时销毁。
运行前先预测:内层大括号结束后,哪些字符串仍被拥有?
cargo run --quiet --example lifetimes
预期输出:
chosen=ownership
left=rust
chosen 的使用发生在 right 被丢弃之前,所以有效。内层结束后 right 被丢弃,外层的 left 仍然存在。
有意让引用越过拥有者的边界
- 在内层大括号之前声明
let chosen;。 - 把内层的
let chosen = ...改为chosen = ...,并把打印chosen的语句移到内层大括号之后。 - 运行
cargo check --example lifetimes,预期失败,通常提示rightdoes not live long enough(E0597)。 - 恢复主示例。即使把
left改得更长,签名仍允许返回right,调用者必须遵守签名给出的关系。
独立练习:如果选择结果必须在两个输入销毁后仍然使用,把函数改成返回 String。只复制最终被选中的内容,不在比较之前克隆两个输入。
完整参考实现
fn longer_owned(left: &str, right: &str) -> String {
let chosen = if left.len() >= right.len() { left } else { right };
chosen.to_owned()
}
fn main() {
let chosen;
{
let left = String::from("rust");
let right = String::from("ownership");
chosen = longer_owned(&left, &right);
}
assert_eq!(chosen, "ownership");
println!("chosen={chosen}");
}
替换实验文件运行,预期 chosen=ownership。to_owned() 把借用的 str 复制为拥有的 String;它的存活期不再依赖输入,因此函数返回类型不需要生命周期参数。
如果想返回函数内 String 的引用,加任意 'a 都不能解决拥有者已经销毁的问题,应返回拥有值或借用调用者提供的数据。如果你想随手改成 &'static str,它要求引用可在整个程序运行期间有效,通常不能由运行时新建的局部字符串满足。先决定数据由谁保存,再写标注,顺序不能颠倒。
本课新增 examples/lifetimes.rs。下一课落地的 Report 只保存整数,主动避免让统计结果绑定输入的生命周期。