3 生命周期省略

原文链接: https://tfpk.github.io/lifetimekata/chapter_3.html

上一章我们了解了为什么需要生命周期。编译器无法自动判断参数或返回值中的引用如何相互关联,因此我们需要告诉编译器这些引用是相关的。

话虽如此,你大概写过需要引用(很可能是 &str)的 Rust 函数,却从未写过生命周期。那时为什么不必标注?Rust 中有一些常见模式,能让编译器清楚推断生命周期应该是什么。让我们看几个例子:

示例 1:返回值中没有引用

1
2
3
4
5
6
7
fn add(a: &i32, b: &i32) -> i32 {
    *a + *b
}

# fn main() {
#     assert_eq!(add(&3, &4), 7);
# }

在这个函数中,a 和 b 的生命周期不必相互关联。假设只有一个线程,且代码是安全的,它们所引用的变量在函数执行期间不可能被 drop;函数返回后,它们想存活多久都可以。

示例 2:输入中只有一个引用

1
2
3
4
5
6
7
8
fn identity(a: &i32) -> &i32 {
    a
}

# fn main() {
#     let x = 52;
#     assert_eq!(&x, identity(&x));
# }

值得注意的是,(通常 {{footnote: 对于 static 类型(如字符串字面量)这是可能的,我们将在后面讲到}} )若引用不是传入的,就不可能创建引用并把它传出函数。因为引用必须指向你拥有的东西,而你拥有的东西在函数结束时会被 drop。因此你拥有的东西无法被引用;唯一能返回引用的方式,就是有人把引用传给了你。

因此,若参数中只有一个引用,你能返回的引用也只有那一个——参数的生命周期必须与返回的引用相同。

怎么办

Rust 本可以把这些情况专门写成例外;但那样可能会有大量例外,规则也会变得令人困惑。

相反,Rust 项目确定了一套编译器遵循的推断生命周期的流程。

编译器首先把函数签名中的所有引用分为两类:「输入」和「输出」。「输入」引用在函数参数(即实参)中;「输出」引用在函数返回类型中。

本章要学的两条规则是:

  1. 每个被省略(即「省略标注」)的输入生命周期,都会填入各自独立的生命周期。
  2. 若所有输入引用上恰好只有一个生命周期,该生命周期会赋给每一个输出生命周期。

看看这些规则如何影响上面两个例子,以及上一章的一个例子:

示例 1:返回值中没有引用

我们有:

1
2
3
fn add(a: &mut i32, b: &mut i32) -> i32 {
    *a + *b
}

有两个输入生命周期,分别对应 a 和 b 的类型。各自分配独立的生命周期:

1
2
3
4
5
6
7
fn add<'elided1, 'elided2>(a: &'elided1 i32, b: &'elided2 i32) -> i32 {
    *a + *b
}

# fn main() {
#     assert_eq!(add(&3, &4), 7);
# }

没有输出生命周期,到此结束。

这个例子现在正确了——a 和 b 的两个生命周期可以完全无关;输出是拥有所有权的值,根本不依赖任何生命周期。

示例 2:输入中只有一个引用

我们有:

1
2
3
4
5
6
7
8
fn identity(a: &i32) -> &i32 {
    a
}

# fn main() {
#     let x = 52;
#     assert_eq!(&x, identity(&x));
# }

只有一个输入生命周期(a 的类型需要):

1
2
3
4
5
6
7
8
fn identity<'elided1>(a: &'elided1 i32) -> &i32 {
    a
}

# fn main() {
#     let x = 52;
#     assert_eq!(&x, identity(&x));
# }

只有一个输出生命周期;且所有输入生命周期共享同一生命周期('elided1);因此可以把所有输出生命周期都设为该生命周期:

1
2
3
4
5
6
7
8
fn identity<'elided1>(a: &'elided1 i32) -> &'elided1 i32 {
    a
}

# fn main() {
#     let x = 52;
#     assert_eq!(&x, identity(&x));
# }

现在就说得通了:返回 &i32 的唯一可能方式,就是从参数得到它;可以看到输入与输出共享同一生命周期。

示例 3:省略标注的局限

再看上一章的这个例子。

1
2
3
4
5
6
7
fn max_of_refs(a: &i32, b: &i32) -> &i32 {
    if *a > *b {
        a
    } else {
        b
    }
}

与示例 1 一样,有两个输入生命周期,因此给它们分配不同的生命周期:

1
2
3
4
5
6
7
fn max_of_refs<'elided1, 'elided2>(a: &'elided1 i32, b: &'elided2 i32) -> &i32 {
    if *a > *b {
        a
    } else {
        b
    }
}

但与示例 1 不同,这里需要输出生命周期!根据第二条规则,只有在恰好有一个输入生命周期时,才能省略输出生命周期。因此 Rust 认为此处省略生命周期是错误——用户必须提供更多信息!

练习:应用这些规则

本练习中有四个函数缺少部分生命周期标注。你的任务是手动遵循生命周期省略规则,按编译器会做的方式为这些函数补上生命周期。

在 lifetimekata 的未来版本中,这些会自动检查。目前完成后,请对照参考答案核对。

最后修改 August 23, 2026: 更新 (499855b16)