《Rust编程实战》系列第39篇
前两篇文章中,我们分别学习了Rust生命周期基础和生命周期详细语法,已经知道
'a并不是让数据活得更久,而是用于描述引用之间的有效范围关系。但生命周期真正难的地方往往不是记住:
rust
fn longest<'a>(
a: &'a str,
b: &'a str,
) -> &'a str
而是面对真实代码时判断:
-
返回值到底借用了谁
-
为什么这个引用不能返回
-
为什么结构体必须写生命周期
-
为什么有时应该返回
String -
为什么某些零拷贝设计需要生命周期
-
为什么一个看似正常的Vec操作会产生借用冲突
本篇不再重复大量理论,而是通过实际案例理解生命周期。
案例一:返回字符串的一部分
先看一个最常见场景:
arduino
fn first_word(text: &str) -> &str {
text.split_whitespace()
.next()
.unwrap_or("")
}
fn main() {
let text = String::from("Rust Language");
let word = first_word(&text);
println!("{}", word);
}
输出:
Rust
这里返回的word并不是新的String,而是指向text内部数据的字符串切片。
生命周期关系可以理解为:
arduino
text ───────────────────
word ───────────
word不能比text存在得更久。
完整生命周期形式可以写成:
rust
fn first_word<'a>(
text: &'a str,
) -> &'a str {
text.split_whitespace()
.next()
.unwrap_or("")
}
不过这里只有一个输入引用,所以Rust可以自动省略生命周期。
案例二:返回较长字符串
如果有两个输入引用:
python
fn longest(
first: &str,
second: &str,
) -> &str {
if first.len() >= second.len() {
first
} else {
second
}
}
编译器无法判断返回值来自哪个参数,因此需要:
rust
fn longest<'a>(
first: &'a str,
second: &'a str,
) -> &'a str {
if first.len() >= second.len() {
first
} else {
second
}
}
使用:
ini
fn main() {
let first = String::from("Rust");
let second = String::from("JavaScript");
let result = longest(&first, &second);
println!("{}", result);
}
返回引用受到两个参数共同有效范围限制。
案例三:生命周期较短的参数
看看下面代码:
ini
fn main() {
let first = String::from("Rust Language");
let result;
{
let second = String::from("Go");
result = longest(&first, &second);
println!("{}", result);
}
}
这是可以的,因为result只在second有效时使用。
但如果改成:
ini
fn main() {
let first = String::from("Rust Language");
let result;
{
let second = String::from("Go");
result = longest(&first, &second);
}
println!("{}", result);
}
就无法通过编译。
即使我们知道:
sql
Rust Language
比:
Go
更长,理论上函数实际会返回first,编译器也不会根据运行结果放宽生命周期规则。
函数签名已经说明:
rust
first: &'a str
second: &'a str
-> &'a str
返回值可能来自任何一个参数,因此必须按安全范围处理。
案例四:返回值只来自第一个参数
如果函数明确只返回第一个参数:
rust
fn select_first<'a, 'b>(
first: &'a str,
second: &'b str,
) -> &'a str {
println!("second = {}", second);
first
}
那么返回值只和:
sql
first
有关。
此时第二个参数即使生命周期更短,也不会限制返回结果。
例如:
ini
fn main() {
let first = String::from("Rust");
let result;
{
let second = String::from("Go");
result = select_first(
&first,
&second,
);
}
println!("{}", result);
}
这是安全的,因为返回值一定来自first。
这个案例说明:
生命周期标注越准确,API受到的不必要限制越少。
案例五:错误地返回局部String引用
这是生命周期最经典的错误之一:
rust
fn create_name() -> &str {
let name = String::from("Tom");
&name
}
函数结束时:
name
立即被释放。
如果允许返回&name:
name释放
↓
返回引用仍存在
↓
悬空引用
即使加生命周期:
rust
fn create_name<'a>() -> &'a str {
let name = String::from("Tom");
&name
}
仍然错误。
正确方式是返回所有权:
rust
fn create_name() -> String {
String::from("Tom")
}
原则非常重要:
函数内部创建的新数据,一般返回拥有所有权的值,而不是返回它的引用。
案例六:结构体保存字符串引用
假设我们解析一篇文章,但不想复制标题:
rust
#[derive(Debug)]
struct Article<'a> {
title: &'a str,
}
fn main() {
let title =
String::from("Rust生命周期详解");
let article = Article {
title: &title,
};
println!("{:?}", article);
}
这里:
css
Article<'a>
表示Article内部保存的是借用数据。
因此:
css
Article不能比title活得更久
错误场景:
css
fn main() {
let article;
{
let title =
String::from("Rust教程");
article = Article {
title: &title,
};
}
println!("{}", article.title);
}
title已经释放,而article.title还想使用,因此编译失败。
案例七:业务实体到底要不要生命周期
假设用户结构体:
rust
struct User<'a> {
name: &'a str,
email: &'a str,
}
这种设计并不是错误,但意味着User依赖外部数据。
例如:
ini
let name = String::from("Tom");
let email = String::from("tom@test.com");
let user = User {
name: &name,
email: &email,
};
如果User要:
-
保存进缓存
-
跨函数长期使用
-
放入Vec
-
返回给其他模块
生命周期管理会逐渐复杂。
这类业务实体通常更适合:
arduino
struct User {
name: String,
email: String,
}
也就是说:
能使用生命周期,不代表一定应该使用生命周期。
案例八:零拷贝配置解析
生命周期非常适合临时解析。
例如配置字符串:
ini
host=127.0.0.1
定义:
rust
#[derive(Debug)]
struct ConfigItem<'a> {
key: &'a str,
value: &'a str,
}
fn parse_config(
input: &str,
) -> Option<ConfigItem<'_>> {
let (key, value) =
input.split_once('=')?;
Some(ConfigItem {
key: key.trim(),
value: value.trim(),
})
}
使用:
arduino
fn main() {
let line =
String::from("host=127.0.0.1");
let config =
parse_config(&line).unwrap();
println!("{:?}", config);
}
这里没有创建新的:
arduino
String
key和value都直接引用line内部数据。
这就是典型的:
rust
Zero Copy
零拷贝
案例九:HTTP Header解析
例如输入:
bash
Content-Type: application/json
可以设计:
rust
#[derive(Debug)]
struct Header<'a> {
name: &'a str,
value: &'a str,
}
fn parse_header(
line: &str,
) -> Option<Header<'_>> {
let (name, value) =
line.split_once(':')?;
Some(Header {
name: name.trim(),
value: value.trim(),
})
}
使用:
rust
fn main() {
let line = String::from(
"Content-Type: application/json"
);
let header =
parse_header(&line).unwrap();
println!("{} = {}",
header.name,
header.value
);
}
解析过程中没有复制Header名称和内容。
在:
-
HTTP解析器
-
CSV解析器
-
编译器
-
日志分析
-
网络协议解析
等性能敏感场景中,这种生命周期设计非常常见。
案例十:从结构体方法返回字段引用
例如:
rust
struct User {
name: String,
}
impl User {
fn name(&self) -> &str {
&self.name
}
}
fn main() {
let user = User {
name: String::from("Tom"),
};
let name = user.name();
println!("{}", name);
}
这里没有写生命周期,但Rust会自动推断:
rust
fn name<'a>(
&'a self,
) -> &'a str
返回值来自self,因此返回引用不能比user活得更久。
这也是生命周期省略规则最常见的实际应用。
案例十一:从Vec返回元素引用
例如查找最大值:
rust
fn max_value(
values: &[i32],
) -> Option<&i32> {
values.iter().max()
}
fn main() {
let values = vec![
10, 30, 20, 100, 50
];
let max = max_value(&values);
println!("{:?}", max);
}
返回的:
rust
&i32
借用自:
perl
values
所以不能让返回引用比Vec存在得更久。
例如:
ini
let result;
{
let values = vec![1, 2, 3];
result = max_value(&values);
}
println!("{:?}", result);
无法通过编译。
案例十二:Vec引用与修改冲突
生命周期不仅影响返回引用,还影响修改。
ini
fn main() {
let mut values =
vec![10, 20, 30];
let first = &values[0];
values.push(40);
println!("{}", first);
}
这里会产生借用冲突。
原因是:
sql
first
仍然借用了Vec内部元素,而:
ini
values.push(40);
可能导致Vec重新分配内存。
如果重新分配,旧的first可能变成悬空引用。
正确方式:
ini
fn main() {
let mut values =
vec![10, 20, 30];
let first = &values[0];
println!("{}", first);
values.push(40);
println!("{:?}", values);
}
first最后一次使用结束后,借用结束,再修改Vec就是安全的。
案例十三:可变引用返回值
例如查找第一个元素并修改:
rust
fn first_mut(
values: &mut [i32],
) -> Option<&mut i32> {
values.first_mut()
}
fn main() {
let mut values = [10, 20, 30];
if let Some(value) =
first_mut(&mut values)
{
*value = 100;
}
println!("{:?}", values);
}
输出:
csharp
[100, 20, 30]
返回的可变引用来源于values,所以它的生命周期受到原数组借用的限制。
案例十四:HashMap查询返回引用
例如:
rust
use std::collections::HashMap;
fn find_user<'a>(
users: &'a HashMap<u32, String>,
id: &u32,
) -> Option<&'a String> {
users.get(id)
}
返回引用来自:
bash
users
而不是:
bash
id
所以只需要让返回生命周期和HashMap绑定。
使用:
rust
fn main() {
let mut users = HashMap::new();
users.insert(
1,
String::from("Tom"),
);
let name = find_user(&users, &1);
println!("{:?}", name);
}
这里没有复制String,只返回HashMap内部数据的引用。
案例十五:返回String还是&str
假设有函数:
scss
fn normalize(text: &str) -> String {
text.trim().to_lowercase()
}
为什么返回String而不是:
python
&str
因为:
scss
to_lowercase()
会创建新的字符串。
新数据并不直接存在于输入text内部,所以应该把所有权返回。
而:
rust
fn trim_text(text: &str) -> &str {
text.trim()
}
可以返回&str,因为结果只是原字符串的一部分。
判断方式:
返回值来自输入已有数据
→ 可以考虑引用
返回值是函数新创建的数据
→ 通常返回所有权
案例十六:生命周期与String Clone的选择
为了绕过生命周期问题,有人可能写:
scss
fn first_word(
text: &str,
) -> String {
text.split_whitespace()
.next()
.unwrap_or("")
.to_string()
}
这样生命周期确实简单了,但产生一次字符串分配和复制。
如果调用量很大,可以使用:
rust
fn first_word(
text: &str,
) -> &str
避免复制。
但如果结果需要脱离原字符串长期保存,那么返回:
arduino
String
反而更合理。
生命周期设计不是单纯追求"零复制",还要考虑API使用难度。
生命周期案例中的核心判断方法
遇到生命周期问题,可以按照下面顺序分析。
第一步:谁拥有数据?
例如:
rust
String拥有字符串
Vec拥有元素
User拥有字段
第二步:引用从哪里来?
例如:
rust
&str来自String
&i32来自Vec
&User来自User变量
第三步:返回引用借用的是哪个参数?
如果答案不明确,可能需要生命周期标注。
第四步:原数据什么时候销毁?
返回引用绝不能超过这个范围。
第五步:是否真的需要返回引用?
如果函数创建了新数据,直接返回:
rust
String
Vec<T>
Struct
往往更合理。
生命周期实战最佳实践
临时读取优先引用
sql
fn show(user: &User)
返回输入的一部分可以使用引用
rust
fn first_word(text: &str) -> &str
新创建的数据返回所有权
php
fn create_user() -> User
临时解析器适合生命周期
rust
struct Parsed<'a> {
value: &'a str,
}
长期业务实体优先拥有数据
arduino
struct User {
name: String,
}
不要为了零拷贝让API过度复杂
如果为了节省一次小型字符串复制,引入大量生命周期参数并让调用代码非常难写,可能得不偿失。
本章小结
生命周期真正的价值,需要结合实际代码才能理解。
本文通过多个案例学习了:
-
返回字符串切片
-
多参数引用选择
-
不同生命周期参数
-
返回局部变量引用为什么错误
-
结构体保存引用
-
业务实体何时应该拥有数据
-
配置零拷贝解析
-
HTTP Header解析
-
方法返回字段引用
-
Vec元素引用
-
Vec修改与引用冲突
-
可变引用返回
-
HashMap查询返回引用
-
String和&str返回值选择
-
Clone与生命周期之间的权衡
可以记住:
生命周期问题首先是所有权问题,其次才是语法问题。
先确定谁拥有数据,再确定谁借用数据,最后判断引用能安全存在多久。
函数新创建的数据通常返回所有权;返回输入数据的一部分时,才考虑返回引用。
下一篇预告
下一篇我们将专门解决Rust开发中最容易遇到的生命周期编译错误:
Rust 生命周期常见错误详解:看懂编译器报错并正确修复
内容包括:
-
missing lifetime specifier -
does not live long enough -
cannot return reference to local variable -
borrowed value does not live long enough -
多参数返回引用错误
-
结构体生命周期错误
-
生命周期与Vec借用冲突
-
生命周期与可变借用冲突
-
'static滥用 -
Clone解决生命周期问题的误区
-
从错误代码一步步修改为正确代码