跳转到内容

Rust 错误 E0106:missing lifetime specifier(缺少生命周期标注)

E0106 的意思是:你在函数的返回类型或结构体里写了一个引用(&),而 Rust 看不出它借用的是谁的数据。 这不是你的逻辑有 bug。编译器只是需要你多写一个词,或者稍微换个思路。几乎每个人在学 Rust 的第一周都会遇到这个错误,修复通常只要一行。(本页属于 Rust 编译错误详解系列。)

error[E0106]: missing lifetime specifier
--> src/main.rs:1:32
|
1 | fn longer(a: &str, b: &str) -> &str {
| ---- ---- ^ expected named lifetime parameter
|
= help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `a` or `b`
help: consider introducing a named lifetime parameter
|
1 | fn longer<'a>(a: &'a str, b: &'a str) -> &'a str {
| ++++ ++ ++ ++

产生这个错误的程序:

fn longer(a: &str, b: &str) -> &str {
if a.len() >= b.len() { a } else { b }
}
fn main() {
println!("{}", longer("apple", "fig"));
}

编译器说:缺少生命周期标注,这里需要一个具名的生命周期参数(“expected named lifetime parameter”)。第一条 help 解释了原因:函数的返回类型里有一个借用的值,但函数签名没有说明它是从 a 借来的还是从 b 借来的。第二条 help 建议你引入一个具名的生命周期参数 'a。

引用就是借用:它指向别人拥有的数据。每当函数返回一个引用,Rust 都需要知道它指向的是谁的数据,这样才能检查调用者使用结果时,那份数据还活着。

看第 1 行下面的三个标记。编译器用 ---- 标出了 a 和 b,用 ^ 标出了返回类型。它是在说:“你返回了一个借来的 &str,它可能来自 a,也可能来自 b。到底是哪个?”编译器不会去猜。

想想图书馆的借书证。朋友递给你一本书,说“这是借来的”,你会想知道是从谁那儿借的,这样才知道什么时候必须还回去。生命周期就是贴在借用上的那张标签。

Rust 保证引用永远不会比它指向的数据活得更久(不会有“悬垂指针”)。为了在函数调用处检查这一点,编译器只看函数的签名,不看函数体。所以签名本身必须说明返回的引用来自哪里。

大多数时候你根本不用写生命周期,因为 Rust 会用三条简单的省略规则(elision rules)自动补上:

  1. 每个引用参数都有它自己的生命周期。
  2. 如果正好只有一个引用参数,返回的引用就借用自它。
  3. 如果是带 &self 或 &mut self 的方法,返回的引用就借用自 self。

当这几条规则都给不出答案时,就会出现 E0106:有两个或更多引用参数(而且没有 self)、一个引用参数都没有,或者引用存放在结构体里。生命周期这一课会深入讲解,借用则讲的是引用本身。

修复 1:加上一个具名的生命周期('a)

Section titled “修复 1:加上一个具名的生命周期('a)”

当函数确实返回某个输入的一部分时,选这个方法。'a 只是一个名字。把它写在输入和输出上,意思是“结果和这些输入活得一样久”:

// 'a:返回的 &str 借用自 a 或 b,所以它和两者活得一样久。
fn longer<'a>(a: &'a str, b: &'a str) -> &'a str {
if a.len() >= b.len() { a } else { b }
}
fn main() {
let first = String::from("apple");
let second = String::from("fig");
println!("更长的单词:{}", longer(&first, &second));
}

输出:

更长的单词:apple
▶ 动手试一试

这里的单词保留英文,是因为 len() 数的是字节:一个汉字占 3 个字节,换成中文比较的就不是字数了。如果结果只会来自一个参数,就只在那个参数上写 'a:fn lookup<'a>(map: &'a HashMap<String, String>, key: &str) -> Option<&'a str>。编译器的建议是给所有地方都加上 'a,这样也能用,但比实际需要的更严格。

修复 2:改为返回拥有所有权的 String

Section titled “修复 2:改为返回拥有所有权的 String”

当函数要生成新的文本,或者你不想让调用者被一个借用绑住时,选这个方法。拥有所有权的 String 属于调用者,所以没有什么需要追踪的:

fn greeting(name: &str, lang: &str) -> String {
if lang == "zh" {
format!("你好,{name}!")
} else {
format!("Hello, {name}!")
}
}
fn main() {
println!("{}", greeting("Ana", "zh"));
println!("{}", greeting("Sam", "en"));
}

输出:

你好,Ana!
Hello, Sam!
▶ 动手试一试

这样做要付出一次小小的内存分配。对大多数程序来说完全没问题,不用为此感到内疚。

修复 3:在结构体里存放自有数据

Section titled “修复 3:在结构体里存放自有数据”

像 title: &str 这样的结构体字段也会报同样的错:

error[E0106]: missing lifetime specifier
--> src/main.rs:2:12
|
2 | title: &str,
| ^ expected named lifetime parameter

对结构体来说,最好的修复几乎总是让它拥有数据:用 String 代替 &str,用 Vec<T> 代替 &[T]。这样结构体想活多久就活多久:

#[derive(Debug)]
struct Book {
title: String, // 拥有所有权,而不是 &str
pages: u32,
}
fn main() {
let book = Book { title: String::from("Dune"), pages: 412 };
println!("{}({} 页)", book.title, book.pages);
}

输出:

Dune(412 页)
▶ 动手试一试

只有当结构体是一个短暂存在的视图,指向存放在别处的数据时(比如一个查看大段文本的解析器),才给它加上生命周期。这时要在结构体和字段上都写 <'a>:

struct Excerpt<'a> {
text: &'a str, // 借用自下面的小说
}
fn main() {
let novel = String::from("Call me Ishmael. Some years ago...");
let first = novel.split('.').next().unwrap();
let excerpt = Excerpt { text: first };
println!("摘录:{}", excerpt.text);
}

输出:

摘录:Call me Ishmael
▶ 动手试一试

小说的原文保留英文,因为代码用英文句号 '.' 来切分句子。

如果函数一个引用参数都没有,报错会说“there is no value for it to be borrowed from”(没有可以借用的值):

error[E0106]: missing lifetime specifier
--> src/main.rs:1:22
|
1 | fn default_name() -> &str {
| ^ expected named lifetime parameter
|
= help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from

如果你只返回 "OK" 这样的字符串字面量,就写 &'static str。字面量直接编进了程序里,所以在整个运行期间都有效:

fn status_text(code: u16) -> &'static str {
match code {
200 => "OK",
404 => "Not Found",
_ => "Unknown",
}
}
fn main() {
println!("200 表示 {}", status_text(200));
println!("404 表示 {}", status_text(404));
}

输出:

200 表示 OK
404 表示 Not Found
▶ 动手试一试

'static 只用于真正的字面量和常量。如果文本是在运行时生成的(用 format!,或者从文件里读出来),'static 帮不上忙:请返回 String(修复 2)。

  • 有两个输入的辅助函数,比如 fn lookup(map: &HashMap<String, String>, key: &str) -> Option<&str>。修复:用 'a 只把输出和 map 绑在一起(修复 1)。
  • 存放文本的结构体,比如用解析出来的输入构建的 User { name: &str }。修复:使用 String 字段(修复 3)。
  • 返回固定消息的函数,比如 fn error_label() -> &str。修复:&'static str(修复 4)。

方法是个好消息:多亏了规则 3,fn name(&self) -> &str 完全不需要写生命周期。

小测验

1. 为什么 fn first_word(s: &str) -> &str 不写任何生命周期也能编译通过?

2. 你的结构体 Config { path: &str } 报了 E0106。通常最好的修复是什么?

3. 一个函数用 format! 生成一条消息并返回它。返回类型应该是什么?

得分: 0 / 3