《Rust编程实战》系列第40篇
前面几篇文章中,我们已经系统学习了Rust生命周期基础、生命周期标注以及生命周期案例。真正开始写Rust代码后,你会发现生命周期最难的地方往往不是
'a语法,而是编译器突然出现一大串错误信息,例如:
vbnet
missing lifetime specifier
borrowed value does not live long enough
cannot return reference to local variable
cannot borrow as mutable because it is also borrowed as immutable
很多初学者看到这些错误后,会直接尝试加'a、加'static或者调用clone()。这些方法有时候能让代码通过编译,但并不一定解决了真正的问题。
生命周期错误的核心通常只有几个:
-
引用比原数据活得更久
-
编译器不知道返回引用来自哪个参数
-
同一数据的借用发生冲突
-
结构体保存的引用生命周期不明确
-
本应该返回所有权,却错误地返回引用
本文通过常见错误逐个分析。
错误一:missing lifetime specifier
先看一个经典例子:
python
fn longest(
first: &str,
second: &str,
) -> &str {
if first.len() >= second.len() {
first
} else {
second
}
}
编译时通常会提示:
r
missing lifetime specifier
为什么?
因为函数有两个输入引用:
yaml
first: &str
second: &str
返回值也是引用:
rust
-> &str
编译器无法确定返回值究竟来自first还是second。
正确写法:
rust
fn longest<'a>(
first: &'a str,
second: &'a str,
) -> &'a str {
if first.len() >= second.len() {
first
} else {
second
}
}
这里表示返回引用受两个输入引用共同有效范围限制。
注意:'a并不是延长两个变量寿命,而是说明它们与返回值之间的关系。
错误二:返回局部变量引用
下面代码非常常见:
rust
fn create_name() -> &str {
let name = String::from("Tom");
&name
}
这段代码不可能安全。
执行过程:
arduino
进入函数
↓
创建String
↓
返回&String中的数据
↓
函数结束
↓
String被释放
如果Rust允许返回这个引用,调用者得到的就是悬空引用。
错误的修复方式是:
rust
fn create_name<'a>() -> &'a str {
let name = String::from("Tom");
&name
}
加'a没有任何作用,因为生命周期标注不能让name继续存在。
正确方式:
rust
fn create_name() -> String {
String::from("Tom")
}
函数创建新数据时,通常应该把所有权返回给调用者。
错误三:borrowed value does not live long enough
例如:
ini
fn main() {
let result;
{
let text = String::from("Rust");
result = &text;
}
println!("{}", result);
}
这里text只在内部代码块中有效。
可以简单表示为:
arduino
text ───────
result ───────────────
result希望比text活得更久,因此编译器拒绝。
正确做法之一是缩短引用使用范围:
ini
fn main() {
{
let text = String::from("Rust");
let result = &text;
println!("{}", result);
}
}
如果结果必须在外层使用,则应该移动所有权:
ini
fn main() {
let result;
{
let text = String::from("Rust");
result = text;
}
println!("{}", result);
}
这里String所有权被移动到result,因此不会产生悬空引用。
错误四:生命周期参数写了却仍然报错
很多初学者认为只要写:
css
'a
生命周期错误就能解决。
例如:
rust
fn get_text<'a>() -> &'a str {
let text = String::from("Hello");
&text
}
仍然会报错。
原因是生命周期标注的作用是:
描述引用之间的关系,而不是创建新的生命周期。
这个函数没有任何输入引用,却声称可以返回任意
'a生命周期的引用,这显然无法保证。正确设计还是:
rust
fn get_text() -> String {
String::from("Hello")
}
错误五:返回值绑定了错误的参数
例如:
rust
fn select<'a>(
first: &'a str,
second: &str,
) -> &'a str {
second
}
这里声明:
sql
返回值生命周期来自first
但实际返回:
sql
second
二者不一致,因此无法通过编译。
如果返回值可能来自两个参数:
rust
fn select<'a>(
first: &'a str,
second: &'a str,
) -> &'a str {
if first.len() > second.len() {
first
} else {
second
}
}
如果返回值始终来自second:
rust
fn select<'a>(
first: &str,
second: &'a str,
) -> &'a str {
println!("{}", first);
second
}
生命周期要描述真实的数据来源关系。
错误六:所有引用都强行使用同一个'a
下面代码虽然可能编译,但生命周期设计并不好:
rust
fn print<'a>(
first: &'a str,
second: &'a str,
) {
println!("{}", first);
println!("{}", second);
}
函数只是打印两个字符串,不返回引用,它们之间根本没有生命周期关系。
直接写:
rust
fn print(
first: &str,
second: &str,
) {
println!("{}", first);
println!("{}", second);
}
即可。
生命周期不是越多越好。
原则是:
只有编译器无法推断,并且引用之间确实存在关系时,才显式标注生命周期。
错误七:结构体引用缺少生命周期
错误:
rust
struct User {
name: &str,
}
结构体可能比name指向的数据存在得更久,因此Rust需要知道两者生命周期关系。
正确:
rust
struct User<'a> {
name: &'a str,
}
使用:
ini
fn main() {
let name = String::from("Tom");
let user = User {
name: &name,
};
println!("{}", user.name);
}
这里User不能比name活得更久。
错误八:结构体中的引用指向短生命周期数据
即使定义正确:
rust
struct User<'a> {
name: &'a str,
}
下面仍然错误:
ini
fn main() {
let user;
{
let name = String::from("Tom");
user = User {
name: &name,
};
}
println!("{}", user.name);
}
因为name已经被释放。
解决方法一是让原数据活得足够久:
ini
fn main() {
let name = String::from("Tom");
let user = User {
name: &name,
};
println!("{}", user.name);
}
另一种更常见的业务设计是让结构体拥有数据:
arduino
struct User {
name: String,
}
这样User不依赖外部字符串生命周期。
错误九:不可变借用与可变借用冲突
例如:
vbnet
fn main() {
let mut text = String::from("Rust");
let first = &text;
text.push_str(" Language");
println!("{}", first);
}
这里first仍然需要使用,所以不可变借用仍然有效。
与此同时:
scss
text.push_str(...)
需要可变借用。
形成:
diff
不可变借用
+
可变借用
冲突。
正确做法:
arduino
fn main() {
let mut text = String::from("Rust");
let first = &text;
println!("{}", first);
text.push_str(" Language");
println!("{}", text);
}
first最后一次使用结束后,借用结束,随后就可以修改text。
错误十:同时存在两个可变借用
错误:
ini
fn main() {
let mut text = String::from("Rust");
let first = &mut text;
let second = &mut text;
first.push('!');
second.push('?');
}
Rust禁止同一数据同时存在两个仍会使用的可变引用。
因为两个位置都可以修改同一数据,可能导致数据竞争。
可以按顺序借用:
ini
fn main() {
let mut text = String::from("Rust");
{
let first = &mut text;
first.push('!');
}
{
let second = &mut text;
second.push('?');
}
println!("{}", text);
}
输出:
Rust!?
错误十一:Vec元素引用与push冲突
经典代码:
ini
fn main() {
let mut numbers = vec![10, 20, 30];
let first = &numbers[0];
numbers.push(40);
println!("{}", first);
}
这里first引用Vec中的元素。
而push()可能导致Vec容量不足,从而重新分配内存。
如果内存位置改变:
sql
first
↓
可能仍指向旧内存
因此Rust禁止这种操作。
改为:
ini
fn main() {
let mut numbers = vec![10, 20, 30];
let first = &numbers[0];
println!("{}", first);
numbers.push(40);
}
在push()前完成引用使用即可。
错误十二:遍历Vec时修改Vec
错误:
ini
fn main() {
let mut numbers = vec![1, 2, 3];
for number in &numbers {
if *number == 2 {
numbers.push(4);
}
}
}
循环:
rust
for number in &numbers
对整个Vec进行了不可变借用。
同时:
scss
numbers.push(4)
需要可变借用,因此冲突。
通常应该分两个阶段:
ini
fn main() {
let mut numbers = vec![1, 2, 3];
let need_add =
numbers.iter().any(|n| *n == 2);
if need_add {
numbers.push(4);
}
println!("{:?}", numbers);
}
先完成读取,再修改集合。
错误十三:HashMap查询后立即修改
类似问题也会出现在HashMap:
rust
use std::collections::HashMap;
fn main() {
let mut users = HashMap::new();
users.insert(1, String::from("Tom"));
let user = users.get(&1);
users.insert(2, String::from("Alice"));
println!("{:?}", user);
}
user仍借用users内部数据,而insert()需要修改HashMap,甚至可能导致内部重新分配。
解决方式是先使用借用:
rust
let user = users.get(&1);
println!("{:?}", user);
users.insert(2, String::from("Alice"));
或者根据业务需求复制需要长期保存的数据:
ini
let user = users.get(&1).cloned();
但不要为了逃避借用规则无条件Clone。
错误十四:把所有生命周期改成'static
遇到生命周期错误时,有人会尝试:
rust
fn get_name() -> &'static str
如果返回的是字符串字面量:
rust
fn get_name() -> &'static str {
"Tom"
}
这是正确的。
但下面错误:
rust
fn get_name() -> &'static str {
let name = String::from("Tom");
&name
}
局部String并不会因为写了'static就永久存在。
'static不是生命周期错误的万能修复方案。
错误十五:混淆&'static T和T:'static
rust
&'static str
表示这个引用整个程序运行期间都可以有效。
而:
vbnet
T: 'static
通常表示类型T不包含生命周期较短的借用引用。
例如:
arduino
String
通常满足'static约束,因为它拥有自己的数据。
但:
ini
let name = String::from("Tom");
这个变量本身仍然会在作用域结束后被销毁。
所以:
vbnet
T:'static
并不代表变量永远不会释放。
错误十六:为了生命周期问题大量Clone
例如原本:
rust
fn print_name(name: &str) {
println!("{}", name);
}
调用时只需要:
scss
print_name(&name);
有些代码却写成:
scss
print_name(&name.clone());
如果函数只是读取数据,这个Clone完全没有必要。
生命周期和借用报错出现时应该依次考虑:
r
能否缩短借用范围
↓
能否改成&T
↓
能否调整数据所有权
↓
是否真的需要Clone
Clone应该是业务确实需要独立副本时的选择,而不是编译错误修复工具。
错误十七:返回引用导致API过度复杂
假设函数从数据库查询用户:
rust
fn load_user<'a>() -> &'a User {
// ...
}
这种设计通常就值得怀疑。
数据库查询产生的是新的用户数据,正常应该返回:
php
fn load_user() -> User
或者:
rust
fn load_user() -> Result<User, Error>
如果数据并不是借用自输入参数,就没有必要强行返回引用。
生命周期问题经常可以通过更合理的所有权设计直接消失。
如何分析生命周期报错
遇到生命周期错误时,不要先修改代码,可以按下面顺序判断。
第一步:找到真正拥有数据的变量。例如:
rust
String
Vec
HashMap
Struct
第二步:找到引用:
rust
&str
&i32
&User
&mut T
第三步:确认引用指向谁。
第四步:查看所有者什么时候销毁。
第五步:查看引用最后什么时候使用。
只要出现:
markdown
引用有效范围
>
所有者有效范围
就一定存在问题。
第六步:判断是否应该返回所有权。如果函数内部创建了新对象,通常返回:
rust
String
Vec<T>
User
而不是引用。
第七步:最后再考虑生命周期标注。
生命周期错误最佳实践
不要和Borrow Checker硬碰硬
编译器报错通常是在说明数据所有权关系存在风险,而不是故意增加代码复杂度。
生命周期标注不能延长变量寿命
'a只是约束关系。
新数据优先返回所有权
rust
fn create() -> String
通常比设计一个无法成立的返回引用更合理。
临时读取优先借用
sql
fn show(user: &User)
避免不必要的Move和Clone。
缩短引用有效范围
用完引用后尽量不再使用,可以减少借用冲突。
复杂生命周期先检查数据模型
如果一个普通业务结构体传播了大量:
rust
<'a, 'b, 'c>
应该考虑它是否本来就应该拥有数据。
本章小结
生命周期错误看起来很多,但核心都围绕引用安全。
本文学习了:
-
missing lifetime specifier的原因 -
返回局部变量引用为什么错误
-
borrowed value does not live long enough -
生命周期标注为什么不能延长变量寿命
-
返回引用必须绑定正确输入参数
-
不要无意义绑定所有输入生命周期
-
结构体保存引用需要生命周期
-
不可变借用和可变借用冲突
-
两个可变引用冲突
-
Vec、HashMap修改导致的引用失效风险
-
'static不能解决所有生命周期问题 -
&'static T和T:'static的区别 -
不要通过大量Clone逃避所有权设计
可以记住:
遇到生命周期错误时,首先问"数据属于谁",其次问"引用指向谁",最后问"谁先被销毁"。
生命周期错误大多数不是
'a写少了,而是数据所有权和引用关系设计得不合理。
下一篇预告
下一篇我们将离开引用生命周期,进入Rust数据建模中最重要的基础类型之一:
Rust Struct结构体详解:定义自己的复杂数据类型
内容包括:
-
什么是Struct
-
结构体定义与实例化
-
字段访问与修改
-
字段初始化简写
-
Struct Update语法
-
元组结构体Tuple Struct
-
单元结构体Unit Struct
-
结构体所有权
-
String与&str字段选择
-
#[derive(Debug)] -
方法与关联函数
-
impl代码块 -
Self与self -
用户、商品、订单实战案例