跳转到内容

Rust 错误 E0716:temporary value dropped while borrowed(临时值在借用期间被丢弃)

E0716 的意思是:你从一个没有变量名的值那里借用了数据,然后在后面的代码行里还在用这个借用。 Rust 会在语句结束时扔掉这种没有名字的值,于是那个借用就指向了空无。它看起来很吓人,其实是最容易修的借用错误之一:通常只要把一行拆成两行就好。(这一页属于 Rust 编译错误详解系列。)

error[E0716]: temporary value dropped while borrowed
--> src/main.rs:2:13
|
2 | let s = String::from("hi").as_str(); // s 借用了一个没有名字的 String
| ^^^^^^^^^^^^^^^^^^ - temporary value is freed at the end of this statement
| |
| creates a temporary value which is freed while still in use
3 | println!("{s}"); // ❌ 那个 String 已经没了
| - borrow later used here
|
help: consider using a `let` binding to create a longer lived value
|
2 ~ let binding = String::from("hi");
3 ~ let s = binding.as_str(); // s 借用了一个没有名字的 String
|

第一行的意思是“临时值在借用期间被丢弃了”。help 建议用一个 let 绑定,创建一个活得更久的值。

产生这个错误的程序是这样的:

fn main() {
let s = String::from("hi").as_str(); // s 借用了一个没有名字的 String
println!("{s}"); // ❌ 那个 String 已经没了
}

临时值(temporary)是在表达式中间创建、却从来没有存进变量的值。在上面那一行里,String::from("hi") 确实创建了一个 String,但没有任何东西拥有它:没有为它写 let 名字 = ...。Rust 会在语句结束时,也就是 ; 的位置,丢弃(清理)临时值。

从左到右读这三个标签:

  1. String::from("hi") 下面的 ^^^^:这里创建了一个临时的 String。
  2. 行尾的 -:在这里,也就是 ; 处,那个 String 被释放了(temporary value is freed at the end of this statement)。
  3. 第 3 行 s 下面的 -:可是 s(一个指向那个 String 内部的 &str)在这里还在被使用(borrow later used here)。

想象一位服务员把你点的菜写在便利贴上,一转身就把便利贴扔了。如果你告诉朋友“菜单写在那张便利贴上”,朋友什么也找不到。你得自己留着那张便利贴,或者把点的菜记在一个不会丢的地方。

信息里的 help: 部分通常就是答案:创建一个变量(rustc 把它叫作 binding),让这个值有一个活得更久的所有者。

这和 E0597 是同一条规则:引用绝不能比它指向的值活得更久,否则它就会变成指向已释放内存的悬垂引用。唯一的区别是:E0597 里的值有名字,在 } 处死去;E0716 里的值没有名字,在 ; 处死去。

在同一条语句里使用临时值完全没问题。println!("{}", String::from("hi").as_str()); 可以编译,因为借用在 ; 之前就结束了。只有当后面的代码行还需要这个借用时,才会出错。参见借用什么时候结束?和生命周期。

有一个很有用的例外,叫作临时值生命周期延长(temporary lifetime extension)。如果你在 let 里直接在临时值前面写 &(或 &mut),Rust 会悄悄让这个临时值和变量活得一样久。所以下面这几行都能编译:

struct Label<'a> {
text: &'a str,
}
fn make_title() -> String {
String::from("收件箱")
}
fn main() {
let greeting = &String::from("你好"); // ✅ 延长了:& 直接写在 let 里
let queue = &mut vec![1, 2, 3]; // ✅ 延长了
queue.push(4);
let label = Label { text: &make_title() }; // ✅ 延长了:& 直接写在结构体字面量里
println!("{greeting} {queue:?} {}", label.text);
}

输出:

你好 [1, 2, 3, 4] 收件箱
▶ 动手试一试

这种延长不会穿过方法调用或函数调用。String::from("hi").as_str()、make_title().as_str()、vec![1, 2].iter() 和 shout(&make_title()) 都是在调用内部借用临时值,所以临时值照样在 ; 处死去,你会得到 E0716。不用去背所有情况:看到 E0716,就给这个值起个名字。

修复 1:用 let 给临时值起个名字

Section titled “修复 1:用 let 给临时值起个名字”

几乎每次都选这个方法。这是编译器给的建议,也是大多数 E0716 的正确答案。先把值存进它自己的变量,再在下一行从这个变量借用。

一个很常见的例子是拆分一行输入。read_command() 返回一个新的 String,.trim() 和 .split(' ') 都从它那里借用,而 Vec<&str> 保存着这些借用:

let parts: Vec<&str> = read_command().trim().split(' ').collect(); // ❌ E0716

Rust 会指着 read_command() 说,它在语句结束时就被释放了。给它起个名字:

fn read_command() -> String {
// 真正的程序里,这里会从 stdin 读一行。
String::from(" add milk eggs \n")
}
fn main() {
let line = read_command(); // 现在这个 String 一直活到 main 结束
let parts: Vec<&str> = line.trim().split(' ').collect();
println!("{parts:?}");
println!("命令:{},物品数:{}", parts[0], parts.len() - 1);
}

输出:

["add", "milk", "eggs"]
命令:add,物品数:2
▶ 动手试一试

处理真实输入时你也会碰到一模一样的情况:对 io::stdin().lines() 写 lines.next().unwrap().unwrap().trim().split(' ').collect(),会因为同样的原因报 E0716。先单独写一行 let line = lines.next().unwrap().unwrap();。参见字符串。

修复 2:存拥有所有权的值,而不是引用

Section titled “修复 2:存拥有所有权的值,而不是引用”

当借用需要活得比你能创建的任何变量都久时,选这个方法,比如放在一个要到处传递的结构体里,或者放进一个在循环里不断填充的 Vec。循环里的 let 每一轮结束都会被丢弃,那样只会把 E0716 变成 E0597。

下面是两个出错的版本:一个结构体保存了来自临时值的 &str,一个循环把新字符串的借用放进 Vec:

let label = Label { text: make_title().as_str() }; // ❌ E0716
let mut names: Vec<&str> = Vec::new();
for name in ["ferris", "corro"] {
names.push(name.to_uppercase().as_str()); // ❌ E0716
}

修法是让结构体和 Vec 拥有它们的字符串。用 String 代替 &str,并去掉 .as_str():

struct Label {
text: String, // 拥有所有权,所以不需要生命周期
}
fn make_title() -> String {
String::from("收件箱")
}
fn main() {
let label = Label { text: make_title() }; // 这个 String 被移动进了 Label
println!("标签:{}", label.text);
let mut names: Vec<String> = Vec::new();
for name in ["ferris", "corro"] {
names.push(name.to_uppercase()); // Vec 拥有每一个新的 String
}
println!("{names:?}");
}

输出:

标签:收件箱
["FERRIS", "CORRO"]
▶ 动手试一试

对新手来说,拥有所有权的字段(String、Vec<T>)几乎总是最省事的选择。带 &str 字段的结构体需要生命周期,而且永远不能比它借用的数据活得更久。参见持有引用的结构体和结构体。

当临时值是一条迭代器链的开头时,选这个方法。下面这两行用 vec![...] 创建了一个 Vec,用 .iter() 借用它,然后把迭代器(或它的结果)留到后面用:

let doubled = &mut vec![1, 2, 3].iter().map(|n| n * 10); // ❌ E0716
let best = vec![72, 95, 88].iter().max(); // ❌ E0716

前面的 &mut 没有用:生命周期延长让迭代器活下来了,却没有让 .iter() 借用的那个 Vec 活下来。有两条出路:

  • 用 into_iter(),让迭代器拥有这个 Vec(rustc 也会这样建议)。这样也不需要 &mut 了,写 let mut 就行。
  • 用一个拥有所有权的结果结束这条链,比如 .copied()、.cloned()、.sum(),或者对普通数字用 .collect::<Vec<_>>(),这样过了 ; 之后就没有任何东西还借用着临时值。
fn main() {
// 迭代器拥有这个 Vec,所以没有借用任何临时值
let mut doubled = vec![1, 2, 3].into_iter().map(|n| n * 10);
println!("{:?}", doubled.next());
println!("{:?}", doubled.next());
// .copied() 把 Option<&i32> 变成 Option<i32>,一个不带借用的值
let best = vec![72, 95, 88].iter().max().copied();
println!("最高分:{best:?}");
}

输出:

Some(10)
Some(20)
最高分:Some(95)
▶ 动手试一试

如果之后还要再用这个 Vec,就回到修复 1:先写 let scores = vec![72, 95, 88];,再用 scores.iter().max()。参见迭代器。

在调用链中间给 Mutex 加锁。 let first = names.lock().unwrap().first(); 会报 E0716,因为锁守卫(lock guard)是一个临时值。它被丢弃时锁就释放了,所以 first 不能继续指向锁里面的数据。修法:先写 let guard = names.lock().unwrap();,再调用 guard.first();或者用 .first().cloned() 一次性把数据复制出来。参见共享状态。

把 &临时值 传给一个返回引用的函数。 对于 fn shout(s: &str) -> &str,let t = shout(&make_title()); 这一行会报 E0716:返回的 &str 借用了那个临时的 String,而生命周期延长穿不过函数调用。修法:先写 let title = make_title();,再调用 shout(&title)。

从函数里返回它。 如果你把 make_title().as_str() 写在一个返回引用(比如 -> &'static str)的函数的最后一行,你得到的会是 E0515(“cannot return value referencing temporary value”,不能返回引用了临时值的值)。修法:直接返回这个 String。

小测验

1. 在 Rust 里,“临时值”(temporary)是什么?

2. let s = String::from("hi").as_str(); 后面跟着 println!("{s}");,报了 E0716。通常怎么修?

3. 多亏了临时值生命周期延长,下面哪一行能编译?

得分: 0 / 3