跳过正文

6. 生命周期:lifetime

·
Rust
目录
rust-lang - 这篇文章属于一个选集。
§ 6: 本文

Rust 所有对象都具有 lifetime

lifetime 是类型的注解参数, 只在编译时检查,运行时不感知:

// 不支持在表达式中使用 lifetime 注解
// error: borrow expressions cannot be annotated with lifetimes
let b = &'a(dyn MyTrait + Send + 'static);

// 注意:对于 &dyn MyTrait 的限界,必须加括号,否则报错
// error: expected expression, found keyword `dyn`
let b = &'a dyn MyTrait + Send + 'static;

lifetime 必须位于其它泛型参数之前,如 <'a, T, T2>

struct Student<'a, T> {
    name: &'a str
    description: &'a mut str // lifetime 注解必须紧随 & 操作符后
    age: i32
}

borrow checker
#

lifetime 是一个相对约束,Rust borrow checker 检查 lifetime 是否有效,否则编译报错。

这里的“有效” 有几类含义:

  • 生命周期之间的 subtype 关系检查:比如 'a: 'b,则表示 'a 的借用或对象的 lifetime 一定要 >= 标记为 ‘b 的借用或对象;

  • 对象实际生命周期与标记的 lifetime 间的关系检查:比如 T: 'a,则 T 的生命周期一定要 >= ‘a 标记的借用或对象;

无论是自动,还是手动添加的 lifetime,如果相互之间存在约束关系,这种约束关系必须显式的定义出来(例外的是一些能自动推导的情况,见后文),否则就会编译报错。

lifetime bound 可以作用于类型 T 和其它 lifetime,对于 trait bound 场景可能还需要使用 HRTB 来定义 lifetime。

  1. <T: 'b> :表示 T 的生命周期比 'b 长。&'b T 隐式表示(编译自动推导) T: 'b

  2. <T: Trait + 'b> :表示 T 实现 Trait 且 T 的生命周期比 ‘b 长。

  • Trait 可以是 Fn/FnOnce/FnMutAsyncFn/AsyncFnOnce/AsyncFnMut 类型的闭包,比如 Fn(&str) -> i32
  1. struct foo<'a: 'b, 'b,T: 'b> (val1: &'a String, val2: &'a String, val3: &'b String, val4: &T):

    • <'a: 'b, 'b>:表示 ‘a 的生命周期比 ‘b 长,需要先写 'a: 'b 再写 ‘b。
    • val1 和 val2 的生命周期一样长, 且比 val3 的生命周期长;
    • val4 的生命周期要比 T 长,进而比 ‘b 长,所以 val4 的生命周期要比 val3 长;
    • struct foo 对象的生命周期不能长于 ‘a 和 ‘b,否则 foo 可能会引用应该销毁的对象;
  2. fn print_refs<'a: 'b, 'b>(x: &'a i32, y: &'b i32) -> &'b String:

    • 函数执行期间 ‘a 和 ‘b 的引用要一直有效,即函数对象的生命周期要比 ‘a 和 ‘b 短;
    • 'a: 'b 表示 ‘a 的生命周期比 ‘b 长,所以 x 的生命周期要比 y 长;
    • 返回值的生命周期要和 y 一样长;
fn add_ref<'a, 'b, T>(a: &'a mut T, b: &'b T) -> &'a T where T: std::ops::Add<T, Output = T> + Copy,
{
    *a = *a + *b;
    a // OK

    // Error: b 的生命周期是 'b,与返回值的声明 'a 不一致。
    // b
}

#[derive(Debug)]
// T:'a 可以隐式推导出 &'a T,所以等效于 struct Ref<'a, T: 'a>(&T);
struct Ref<'a, T: 'a>(&'a T);

// 等效于 fn print_ref<'a, T>(t: &T) where T: Debug{
fn print_ref<'a, T>(t: &'a T) where T: Debug + 'a {
    println!("`print_ref`: t is {:?}", t);
}

示例:闭包限界:

pub fn spawn<F, T>(f: F) -> JoinHandle<T>
where
    // FnOnce() -> T 代表一个闭包类型,是一个整体,
    // Send + 'static 是对闭包 FnOnce() -> T 的整体要求,而不是对返回值 T 的要求
    F: FnOnce() -> T + Send + 'static,
    // 返回值 T 的要求
    T: Send + 'static,

对于闭包整体限界 Send + 'static 的理解:

  1. Rust 在定义闭包时,即创建一个实现 Fn/FnMut/FnOnce trait匿名类型对象(如 impl Fn*) ,该对象可能通过借用捕获了上下文对象,所以该匿名类型对象本身是具有 lifetime 约束的(比如它的 lifetime 比所捕获的对象长 ),而上面的 'static 则要求该匿名类型对象是可以在程序运行过程中一直存在,所以一般不能通过借用捕获上下文(需要 move)。而 Send 则 要求该闭包匿名对象可以在 多个线程间转移,所以 move 捕获的对象也必须实现 Send,例如不能是 Rc。(但是线程内创建的 Rc 不 受影响,因为它只能在所在线程运行 )。

  2. 对于异步闭包限界,要求闭包闭包内所有不能有跨 .await 点未实现 Send 的对象,所以异步闭包的输入参数(如 &T)、内部创建的对象、捕获的对象都需要实现 Send,该闭包才从整体上实现 Send。

struct/enum lifetime
#

  • 如果 struct/enum 有引用成员,则必须要为 struct/enum 整体指定 lifetime 参数;
  • struct/enum 对象的 lifetime 要比指定的所有 lifetime 参数短;
  • 特殊的 struct MyStruct<'static> 表示 MyStruct 对象的 lifetime 可以任意长(因为 'static 是在程序整个运行期间都有效);
#[derive(Debug)]
struct Borrowed<'a>(&'a i32);

#[derive(Debug)]
struct NamedBorrowed<'a> {
    x: &'a i32,
    y: &'a i32,
}

#[derive(Debug)]
enum Either<'a> {
    Num(i32),
    Ref(&'a i32),
}

fn main() {
    let x = 18;
    let y = 15;

    let single = Borrowed(&x);
    let double = NamedBorrowed { x: &x, y: &y };
    let reference = Either::Ref(&x);
    let number    = Either::Num(y);

    println!("x is borrowed in {:?}", single);
    println!("x and y are borrowed in {:?}", double);
    println!("x is borrowed in {:?}", reference);
    println!("y is *not* borrowed in {:?}", number);
}

函数 lifetime
#

  1. 借用类型的函数参数和返回值,必须有 lifetime 注解,如果没有显式指定,则编译器自动添加和推断;
  2. 所有借用类型返回值的 lifetime 必须和某些输入的值的 lifetime 相关(如 &self),或者是 'static;
  3. 如果自动推断后还是不能确定借用类型返回值和输入值 lifetime 的关系,则编译报错,这时需要手动设置 lifetime;
fn print_refs<'a, 'b>(x: &'a i32, y: &'b i32) {
    println!("x is {} and y is {}", x, y);
}

fn main() {
    let (four, nine) = (4, 9);

    // &four、&nine 的生命周期必须比 print_refs 执行的时间长,也就是在函数执行期间,four 和 nice 对象的引用要一直有效。
    print_refs(&four, &nine);

    failed_borrow(); // 报错。
}

// 传入的 x 的生命周期要比 print_one 函数长
fn print_one<'a>(x: &'a i32) {
    println!("`print_one`: x is {}", x);
}

fn add_one<'a>(x: &'a mut i32) {
    *x += 1;
}

fn print_multi<'a, 'b>(x: &'a i32, y: &'b i32) {
    println!("`print_multi`: x is {}, y is {}", x, y);
}

// 返回值的 lifetime 必须来源于输入参数的 lifetime 或 'static
fn pass_x<'a, 'b>(x: &'a i32, _: &'b i32) -> &'a i32 { x }

fn failed_borrow<'a>() {
    let _x = 12;

    // ERROR: `_x` does not live long enough
    let _y: &'a i32 = &_x;

    // 这是由于 &_x 的借用有效期持续到函数返回,而 'a 的有效期要长于函数。
}

// 错误:返回值的 lifetime 和 'a 一样长, 而函数内的 ref 在函数返回即失效, 所以编译报错。
fn invalid_output<'a>() -> &'a String { &String::from("foo") }

// 错误:编译器不能推断出返回值引用的 lifetime 关系。
fn order_string(s1 : &str, s2 : &str) -> (&str, &str) {
    if s1.len() < s2.len() {
        return (s1, s2);
    }
    return (s2, s1);
}

函数 fn 也是通过一种类型指针,它的生命周期不能比传入的参数长。

liftime subtype
#

lifetime 之间存在 subtype 关系 ,即更长的 lifetime 是更短 lifetime 的子类型,更长的 lifetime 可以被 coerced 到短的 lifetime:

  • 'a: 'b :表示 ‘a 比 ‘b 长,在返回 ‘b 时可以返回 ‘a 的 lifetime 对象;
  • 'static : 在程序运行期间(甚至 main 函数返回)一直有效,所以是其它任意 lifetime 的子类型;
  • HRTB :HRTB 声明的 lifetime 是任意短的,其它任意指定的 lifetime 都是它的子类型, 可以赋值给 HRTB 标记的对象(如闭包);
fn choose_first<'a: 'b, 'b>(first: &'a i32, _: &'b i32) -> &'b i32 {
    first // 'a 被 coreced 到更短的 'b
}

fn main() {
    let first = 2; // Longer lifetime
    {
        let second = 3; // Shorter lifetime

        println!("The product is {}", multiply(&first, &second));
        println!("{} is the first", choose_first(&first, &second));
    };
}

trait MyTrait<'a> {
    fn say_hello(&'a self) -> &'a String;
}

struct MyStruct(String);

impl<'a> MyTrait<'a> for MyStruct {
    fn say_hello(&'a self) -> &'a String {
        println!("hello {}", self.0);
        &self.0
    }
}

fn printf_hello<'a, 'b>(say_hello: Option<&'a (dyn MyTrait<'a> + Send + 'b)>) -> Option<&'b String> where 'a: 'b,
{
    let hello = if let Some(my_trait) = say_hello {
        my_trait.say_hello()
    } else {
        return None;
    };

    Some(hello)
}

隐式 liftime bound:&'a T 隐式表示 T: 'a, 即 T 的生命周期比 ‘a 长:

fn requires_t_outlives_a_not_implied<'a, T: 'a>() {}

// OK:T 的生命周期比 'a 长
fn requires_t_outlives_a<'a, T>(x: &'a T) {
    requires_t_outlives_a_not_implied::<'a, T>();
}

// 错误:T 的生命周期不满足比 'a 长的约束
fn not_implied<'a, T>() {
    requires_t_outlives_a_not_implied::<'a, T>();
}

struct Struct<'a, T> {
    field: &'a T, // T 的生命周期比 'a 长
}

enum Enum<'a, T> {
    SomeVariant(&'a T), // T 的声明周期比 'a 长
    OtherVariant,
}

trait Trait<'a, T: 'a> {}

impl<'a, T> Trait<'a, T> for () {} // 错误:T 的声明周期不一定比 'a 长
impl<'a, T> Trait<'a, T> for &'a T {} // OK

使用 std::mem::transmute() 来扩充或缩短 lifetime,因为该函数转换后的对象的 lifetime 是显式指定的:

struct R<'a>(&'a i32);

unsafe fn extend_lifetime<'b>(r: R<'b>) -> R<'static> {
    std::mem::transmute::<R<'b>, R<'static>>(r)
}

unsafe fn shorten_invariant_lifetime<'b, 'c>(r: &'b mut R<'static>) -> &'b mut R<'c> {
    std::mem::transmute::<&'b mut R<'static>, &'b mut R<'c>>(r)
}

HRTB 语法和自动生成
#

HRTB 有两种等效等效语法,区别在于 lifetime 的 scope 不同:

// 语法 1:'a 的 scope 是 Fn 的签名
F: for<'a> Fn(&'a (u8, u16)) -> &'a u8
// 等效于
T: (for <'a> Fn(&'a i32) -> i32) + 'b + TraitA

// 语法 2:'a 的 scope 是整个 F 限界
for <'a> F: Fn(&'a (u8, u16)) -> &'a u8

HRTB 也可以用于 fn 函数指针的 lifetime 声明:

// HRTB 为函数指针 fn 的参数和返回值指定 lifetime
fn use_fn_ptr(f: for<'a> fn(&'a str) -> &'a str) {
    let s = "example";
    let result = f(s);
    println!("{}", result);
}
let test_fn: for<'a> fn(&'a _) -> &'a _ = |p: &String| p;
println!("Results:{}", test_fn(&"asdfab".to_string()));

自动生成 HRTB:

  • 对于闭包限界,自动生成 HRTB 后,进一步根据 lifetime-elision 将输出的 lifetime 和输入关联起来:
  • 对于闭包定义:不支持 HRTB 和 lifetime-elision ,实际上,闭包定义不支持任何 lifetime 标记。
// 1. 参数中有引用
fn foo<F>(f: F)
where
    F: Fn(&u8),    // 自动变成 for<'a> Fn(&'a u8)
{
    let x = 1;
    f(&x);
}

// 2. 引用出现在参数的嵌套类型里
fn foo<F>(f: F)
where
    F: Fn(&(u8, u16)),   // 自动变成 for<'a> Fn(&'a (u8, u16))
{ ... }

// 3. 返回值依赖参数生命周期
fn foo<F>(f: F)
where
    F: Fn(&u8) -> &u8,   // 自动变成 for<'a> Fn(&'a u8) -> &'a u8
{ ... }

// 4. 多参数引用: 不同参数的引用生命周期会独立泛化(for<'a, 'b>)。
fn foo<F>(f: F)
where
    F: Fn(&u8, &str) -> &str,
    // 自动变成 for<'a, 'b> Fn(&'a u8, &'b str) -> &'b str
{ ... }

不自动生成 HRTB 的情况:

// 1. 参数没有引用
fn foo<F>(f: F)
where
    F: Fn(u8),   // 不需要 HRTB
{ ... }

// 2. 引用的生命周期显式固定: 这里不会生成 for<'a>,因为 'a 已经是函数泛型参数的一部分。
fn foo<'a, F>(f: F, x: &'a u8)
where
    F: Fn(&'a u8),   // 生命周期已由外层 'a 绑定
{ ... }

HRTB 示例
#

下面对泛型类型 F 没有加 lifetime 的代码是可以正常编译的:

struct Closure<F> {
    data: (u8, u16),
    func: F,
}

impl<F> Closure<F>
where
    F: Fn(&(u8, u16)) -> &u8,
{
    fn call(&self) -> &u8 {
        (self.func)(&self.data)
    }
}

fn do_it(data: &(u8, u16)) -> &u8 { &data.0 }

fn main() {
    let clo = Closure { data: (0, 1), func: do_it };
    println!("{}", clo.call());
}

这是由于对于这种包含没有显式指定引用的 lifetime 限界形式 F: Fn(&(u8, u16)) -> &u8 ,Rust 编译器内部自动生成一个 HRTB(higher-rank trait bound), 同时应用 lifetime-elide 规则将输出和输入的 lifetime 关联起来:

F: Fn(&(u8, u16)) -> &u8
// 被编译器自动翻译为 HRTB 版本
// 表示:对任意生命周期 'a,F 都能接受 &'a (u8, u16) 作为参数,并返回一个 &'a u8。
for<'a> F: Fn(&'a (u8, u16)) -> &'a u8

// 执行 rustc -Zunpretty=hir 命令,可以看到编译器展开后的真实约束
impl<F> Closure<F>
where
    for<'a> F: Fn(&'a (u8, u16)) -> &'a u8,
{
    fn call(&self) -> &u8 {
        <F as Fn<(& (u8, u16),)>>::call(&self.func, (&self.data,))
    }
}

如果不加 for<‘a>,F 就只能匹配某个固定的生命周期(例如 ‘static),无法像普通函数那样在不同调用中接收不同生命周期的引用。

fn foo<F>(f: F)
where
    F: Fn(&(u8, u16)) -> &u8,  // 编译器自动加了 for<'a>
{
    let t1 = (1u8, 2u16);
    let t2 = (3u8, 4u16);
    println!("{}", f(&t1));
    println!("{}", f(&t2));
}

// 如果没有自动加 for<'a>,f 在第一次调用绑定了某个生命周期 'x,第二次调用传入的引用 'y 可能就不兼容了,代码会直接编译失败。

如果要给上面的代码添加 lifetime bound ,则会遇到 F 的 lifetime 该如何指定的问题:

struct Closure<F> {
    data: (u8, u16),
    func: F,
}

impl<F> Closure<F> // where F: Fn(&'??? (u8, u16)) -> &'??? u8,
{
    fn call<'a>(&'a self) -> &'a u8 {
        (self.func)(&self.data)
    }
}

// Error:表达式 &'b data.0 不能指定 lifetime
//fn do_it<'b>(data: &'b (u8, u16)) -> &'b u8 { &'b data.0 }

fn do_it<'b>(data: &'b (u8, u16)) -> &'b u8 { &data.0 }

fn main() {
    'x: {
        let clo = Closure { data: (0, 1), func: do_it };
        println!("{}", clo.call());
    }
}


// Error1:
impl<'a, F> Closure<F> where F: Fn(&'a (u8, u16)) -> &'a u8,
{
    fn call<'a>(&'a self) -> &'a u8 {
        // lifetime name `'a` shadows a lifetime name that is already in scope
        (self.func)(&self.data)
    }
}

// Error2:
impl<'a, F> Closure<F> where F: Fn(&'a (u8, u16)) -> &'a u8,
 {
     fn call<'b>(&'b self) -> &'b u8 {
         //  method was supposed to return data with lifetime `'a` but it is returning data with lifetime `'b`
         (self.func)(&self.data)
     }
 }

// Error3:
impl<'a, F> Closure<F> where F: Fn(&'a (u8, u16)) -> &'a  u8,
{
    fn call(&self) -> &u8 {
        // 编译器自动为 &self 添加 liefitime 如 '1:
        // method was supposed to return data with lifetime `'1` but it is returning data with lifetime `'a`
        (self.func)(&self.data)
    }
}

// Error4: 可以编译过,但是要求 Closure 对象的 liefitime 和传入的 Fn 的参数 lifetime 一致,不符合预期语义: Fn 的函数有自己独立的 lifetime,和
// Closure 对象 lifetime 无关。
impl<'a, F> Closure<F> where F: Fn(&'a (u8, u16)) -> &'a  u8,
{
    fn call(&'a self) -> &'a u8 {
        (self.func)(&self.data)
    }
}

// Error5: argument requires that `'b` must outlive `'a
impl<'a: 'b, 'b, F> Closure<F> where  F: Fn(&'a (u8, u16)) -> &'a u8,
{
    // 函数的参数要求 'b: 'a, 但是函数的返回值要求 'a: 'b, 相互矛盾,所以编译失败。
    fn call(&'b self) -> &'b u8 { (self.func)(&self.data) }
}

// OK:HRTB
// 1. 函数 Closure 泛型参数中没有 'a lifetitme
//
// 2. 在 F 的 Bound 中使用 for <'a> 来声明 'a lifetime,这里 for <'a> 表示对于任意 lifetime 'a, Fn 都满足, 也即 for <'a> 的
// 'a 可以看作无限长,可以是任何其他 lifetime 的 subtype
impl<F> Closure<F> where F: for <'a> Fn(&'a (u8, u16)) -> &'a u8,
{
    fn call<'a>(&'a self) -> &'a u8 {
        // 'a 和上面的 for <'a> 没有关系,是 call() 方法自己的 lifetime。
        // 由于编译器会自动加 lifetime,所以可以不指定 'a 如:fn call(&self) -> &u8
        (self.func)(&self.data)
    }
}

注意:闭包限界不一定一定要使用 HRTB lifetime,也可以使用特定的 lifetime 标记

fn apply_fnmut_simple<'a, 'b, F>(
    mut f: F,
    x: &'a i32,
    y: &'b str,
) -> Combined<'a, 'b>
where
    // 这里闭包限界没有使用 HRTB,这要求 F 的输入参数必须和 x、y 的 lifetime 一致。
    F: FnMut(&'a i32, &'b str) -> Combined<'a, 'b>,
{
    f(x, y)
}

闭包限界和闭包定义的 liftime 问题
#

  1. 闭包限界:支持 HRTB,支持 lifetime elision

  2. 闭包定义:不支持 lifetime 声明和 HRTB,也不支持 lifetime elision, 这样当闭包的输入、输出包含借用,或者闭包通过借用捕获上下文 对象时会有问题。

具体参考:9-rust-lang-function-closure.md

async fn 的闭包限界不支持 HRTB
#

async fn 参数的闭包限界不支持 HRTB。

Rust 1.75 开始支持的 AsyncFn* 闭包限界支持 HRTB

具体参考:14-rust-lang-async.md

‘static
#

&'static 表示借用对象可以在程序运行期间一直有效(即使 main 函数返回也还有效),但是实际可能并不一定是这样。

例如全局 const/static 常量,字符串字面量等,它们都保存在程序二进制的 read-only 部分,在程序运行期间一直有效。

let s: &'static str = "hello world"; // &str 默认是 'static,所以可以忽略

fn generic<T>(x: T) where T: 'static {}

fn need_static(r : &'static str) {
    assert_eq!(r, "hello");
}

fn main() {
    let v: &'static string = "hello";
    need_static(v);
    println!("Success!")
}

'static 是任意其它 lieftime 的子类型,所以可以被 coerced 到一个更短的生命周期:

static NUM: i32 = 18;

fn coerce_static<'a>(_: &'a i32) -> &'a i32 {
    &NUM
}

‘static 的两种解释
#

  1. 转移对象所有权时,对应的 Bound 会隐式的具有 'static lifetime,例如 Box<T> 等效为 Box<T + 'static>

  2. 引用在程序的整个运行期间一直有效,如全局 const/static 常量,函数内定义的 static 常量等;

  • 'static 在引用场景下,代表引用存在于整个程序运行期间有效。

在对象赋值、闭包 move 等语义下,接收方拥有传入的对象的所有权,这时该接收方也实现了 'static。 如 Box<dyn MyTrait> 会将 trait object 的所有权转移到返回的 Box 对象中,所以传入的 trait object 必须是 'static 也即 Box<dyn MyTrait + 'static >

  • 另一种解释:type H contains no references to the current stack (H: ‘static)
use std::fmt::Debug;

// 函数 hold input 值,所以 T 的 Bound 会隐式的自动加 'static 并自动满足。
fn print_it<T: Debug + 'static>(input: T) {
    println!("'static value passed in is: {:?}", input);
}

// 函数 hold input 值,所以 input 的 Bound 会隐式的加 'static 并自动满足。
fn print_it1(input: impl Debug + 'static) {
    println!("'static value passed in is: {:?}", input);
}

// input 借用的 T 值必须是 'static,在程序的整个生命周期都有效(即使 main 函数返回)。
fn print_it2<T: Debug + 'static>(input: &T) {
    println!("'static value passed in is: {:?}", input);
}

fn main() {
    let i = 5;
    print_it(i);

    // i 的 lifetime 持续到 main() 函数返回,而非整个程序声明周期,所以不满两个函数泛型限界的要求。
    print_it(&i); //  `i` does not live long enough
    print_it1(&i); //  `i` does not live long enough

    // but this one WORKS !
    print_it2(&i);
}

‘static 闭包或限界
#

在多线程场景中,提交的闭包必须实现 Send + 'static,闭包返回值也必须实现 Send + 'static

pub fn spawn<F, T>(f: F) -> JoinHandle<T>
where
    F: FnOnce() -> T + Send + 'static, // 注意:Send + 'static 是对闭包 FnOnce() -> T 的整体要求,而不是对返回值 T 的要求
    T: Send + 'static, // 返回值 T 的要求

另外,在异步场景,各种 spawnXX() 函数提交的 Future 对象,也需要实现 Send + 'static:

  • spawn(future):多线程来调度执行,future 需要实现 Future+Send+'static
  • spawn_local(future): 单线程执行,future 需要实现 Future+'static
  • spawn_blocking(closure): 多线程执行,closure 需要实现 Send+'static(注意没有 Future,它是同步闭包);

由于闭包定义不支持 lifetime 声明和 HRTB,所以在闭包的输入、输出、捕获的上下文对象包含借用时大概率会编译出错(因为缺少这三类 lifetime 间的约束关 系定义)。 另外,这些借用很难满足 ‘static 要求,所以一般需要使用 move 闭包定义 + fn 函数的解决方案(参考前文)

// 使用外围工具函数声明 lifetime,然后在闭包定义中使用
fn testStr<'a> (input: &'a String) -> &'a String {
    let closure_test = |input: &'a String | -> &'a String {input};
    return closure_test(input);
}

// 使用函数指针
fn apply_fn_ptr<'a, 'b>(
    f: fn(&'a i32, &'b str) -> Combined<'a, 'b>,
    x: &'a i32,
    y: &'b str,
) -> Combined<'a, 'b> {
    f(x, y)
}

// 不使用 HRTB
fn apply_fnmut_simple<'a, 'b, F>(
    mut f: F,
    x: &'a i32,
    y: &'b str,
) -> Combined<'a, 'b>
where
    F: FnMut(&'a i32, &'b str) -> Combined<'a, 'b>,
{
    f(x, y)
}

// 使用 HRTB
fn apply_fnmut_simple_hrtb<'a, 'b, F>(
    mut f: F,
    x: &'a i32,
    y: &'b str,
) -> Combined<'a, 'b>
where
    F: for<'x, 'y> FnMut(&'x i32, &'y str) -> Combined<'x, 'y>,
{
    f(x, y)
}

Box<dyn Fn(&str) -> V + ‘static>
#

对于闭包限界,如 C: Fn(&i32) -> i32 + 'static 的含义是传入的闭包对象的生命周期可以在函数运行期间一直有效。

注意:这里是满足“可以” 的要求,但并不代表该闭包对象实际的生命周期在程序运行期间一直有效。

如果闭包定义生成的匿名闭包对象满足 'static 要求,则:

  • 不能捕获任何非 ‘static 的引用(可以通过 move 获取值的所有权)
  • 输出的值如果包含引用,则必须是 ‘static 类型;
  1. 输入参数包含非 ‘static 引用:可以满足 ‘static,函数签名中的输入参数生命周期不影响闭包本身的生命周期,这是因为闭包对象 vs 函数调用是分离的:
  • 闭包定义:编译器立即生成匿名对象,而 ‘static 是针对该匿名对象而言的。
  • 函数用时的参数生命周期是在调用时才确定的,与闭包对象的生命周期无关。
// 这个闭包对象本身是 'static 的
let closure: Box<dyn Fn(&str) -> String + 'static> =
    Box::new(|s: &str| s.to_string());

// 但调用时,参数的生命周期可以是任意的
{
    let temp_string = "hello".to_string();
    let result = closure(&temp_string); // temp_string 不是 'static,但这没问题
} // temp_string 在这里被销毁,但不影响 closure 对象


// 另一个例子:
fn create_processor() -> Box<dyn Fn(&str) -> usize + 'static> {
    // 创建一个可以存活任意长时间的闭包对象
    Box::new(|s: &str| s.len()) // ✅ 闭包对象本身不依赖任何外部数据
}
fn main() {
    let processor = create_processor(); // processor 可以存活任意长时间

    // 在不同的作用域中调用,参数生命周期各不相同
    {
        let short_lived = "temp".to_string();
        println!("{}", processor(&short_lived)); // 参数生命周期短,但没问题
    }

    {
        let another_temp = "another".to_string();
        println!("{}", processor(&another_temp)); // 另一个短生命周期参数
    }

    // processor 仍然有效,可以继续使用
    println!("{}", processor("static string")); // 'static 参数也可以
}
  1. 输出参数包含非 ‘static 引用: 不能满足 ‘static,因为将闭包对象和输出引用的对象的生命周期关联起来。如果返回值包含引用,那些引用必须是 ‘static 的:
// ❌ 编译错误
fn bad_example() -> Box<dyn Fn() -> &str + 'static> {
    let s = "hello".to_string();
    Box::new(move || s.as_str()) // s 不是 'static
}

// ✅ 正确
fn good_example() -> Box<dyn Fn() -> &'static str + 'static> {
    Box::new(|| "hello") // 字符串字面量是 'static
}
  1. 通过共享借用捕获上下文对象: 不能满足 ‘static,因为它将闭包对象的生命周期和捕获的对象的生命周期关联起来,这是最关键的限制:
fn example() {
    let data = vec![1, 2, 3];

    // ❌ 编译错误:data 的生命周期不是 'static
    let closure: Box<dyn Fn() -> usize + 'static> =
        Box::new(|| data.len());

    // ✅ 正确:通过 move 获取所有权
    let closure: Box<dyn Fn() -> usize + 'static> =
        Box::new(move || data.len());
}

// 另一个例子:
fn demonstrate_difference() {
    let data = "some data".to_string();

    // ❌ 捕获引用 - 不能是 'static
    // 因为闭包对象依赖于 data 的生命周期
    let bad_closure: Box<dyn Fn() -> &str + 'static> =
        Box::new(|| data.as_str()); // 编译错误

    // ✅ 接受参数 - 可以是 'static
    // 因为闭包对象本身不依赖任何外部数据
    let good_closure: Box<dyn Fn(&str) -> String + 'static> =
        Box::new(|s: &str| s.to_uppercase());

    // good_closure 可以在 data 销毁后继续存在
}

总结:‘static 限界只约束闭包对象本身的生命周期,不约束函数调用时参数的生命周期。这种设计让我们可以创建长期存活的函数对象,同时保持调用时的灵活性。

impl BasicRouter {
    fn add_route<C>(&mut self, url: &str, callback: C)
        // 由于 Box<T> 等效于 Box<T+'static> ,所以要求闭包是 'static
        where C: Fn(&i32) -> i32  + 'static
      {
          self.routers.insert(url.to_string(), Box::new(callback));
      }
}

Box/String 的 leak() 方法返回 ‘static 借用
#

该方法返回一个可以自定义 lifetime 的 &‘a mut 借用类型,常用于返回一个 'static 借用。

Box::leak():

  • 编译器释放 Box 中的对象所有权,并承诺永远不会 drop 它。所以返回的借用在程序整个生命周期内一直有效,相当于是 'static;
  • 缺点:它是内存泄露机制,需要小心使用!
// 'a 是自定义的参数
pub fn leak<'a>(self) -> &'a mut str

// 示例:
let x = String::from("bucket");
// 指定 'a 为 'static
let static_ref: &'static mut str = x.leak();
assert_eq!(static_ref, "bucket");

lifetime elision rule
#

参考:https://doc.rust-lang.org/reference/lifetime-elision.html#lifetime-elision-in-functions

变量的 lifetime 开始于它创建,结束于它被销毁。Rust borrow checker 使用 lifetime annotation 检查所有借用操作,从而确保所有的 borrow 都 是有效的。

由于 lifetime elision rule 的存在,一般不需要显式指定 lifetime annotation:

// 不需要显式指定 lifetime annotation
fn first_word(s: &str) -> &str {
    let bytes = s.as_bytes();
    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[0..i];
        }
    }

    &s[..]
}

lifetime elision rule 适用于普通函数、函数指针和作为限界的闭包 trait 签名,但不适用于闭包函数定义(这会带来一些列问题,参考 9-rust-lang-function-closure.md)

  1. 针对函数的输入参数:编译器自动为所有引用的输入参数设置一个 lifetime:

       struct S<'a, 'b> {
           x: &'a i32,
           y: &'b i32
       }
       fn sum_r_xy(r: &i32, s: S) -> i32 {
           r + s.x + s.y
       }
       // 等效为:
       fn sum_r_xy<'a, 'b, 'c>(r: &'a i32, s: S<'b, 'c>) -> i32
    
  2. 针对函数的输出参数:如果函数 有且只有一个 输入引用类型参数,则该 lifetime 会设置为所有输出引用参数:

       fn first_third(point: &[i32; 3]) -> (&i32, &i32) {
           (&point[0], &point[2])
       }
       // 等效为
       fn first_third<'a>(point: &'a [i32; 3]) -> (&'a i32, &'a i32)
    
  3. 针对方法的输出参数:如果函数是方法,即第一个参数类型是 &self 或 &mut self,则它的 lifetime 会设置到所有输出引用参数。

       struct StringTable {
           elements: Vec<String>,
       }
    
       impl StringTable {
           fn find_by_prefix(&self, prefix: &str) -> Option<&String> {
               for i in 0 .. self.elements.len() {
                   if self.elements[i].starts_with(prefix) {
                   // [i] 返回对象本身,这里需要通过 & 获得它的引用
                       return Some(&self.elements[i]);
                   }
               }
               None
           }
       }
    
       // 等效为
       fn find_by_prefix<'a, 'b>(&'a self, prefix: &'b str) -> Option<&'a String>
    

如果经过上面 elision rule 后,如果还有引用参数 lifetime 不明确,Rust 编译出错:

// OK
fn first_word(s: &str) -> &str {
// 编译器等效为
fn first_word<'a>(s: &'a str) -> &'a str {

fn longest(x: &str, y: &str) -> &str { // 错误,经过 elision rule 后输出引用 lifetime 不明确
// fn longest<'a, 'b>(x: &'a str, y: &'b str) -> &str {

fn get_str() -> &str; // 错误:不能推断返回值 lifetime

‘_ 和使用场景
#

'_ 是用于指示编译器在需要 lifetime 的地方自动推导 lifetime(如根据 lifetime-elision):

  1. 类型定义包含 lifetime 参数,但是实现 trait 时,该 trait 的方法或函数并不依赖 lifetime;
  2. 对于满足 lifetime-elision 的场景,显式的告诉编译器该类型对象需要 lifetime,而且由编译器自动推导;
  • 对于 path 的 lifetime,优选 '_
// 场景 1:// BufReader 类型需要指定 lifetime 参数,但是在实现 Read 时并没有使用该 lifetime 参数
impl<'a> Reader for BufReader<'a> {
    // 'a is not used in the following methods
}
// 等效于
impl Reader for BufReader<'_> {
}

// 场景 2:
impl<T> Vec<T> {
    pub fn iter(&self) -> Iter<'_, T> {
        // 编译器自动推导:返回的 Iter 的 lifetime 和 &self 一致
        // [...]
    }
}

// 场景 2:
struct NumRef<'a> {
    x: &'a i32,
}

// 返回的 NumRef 类型包含 lifetime 参数,而且该函数满足 lifetime-elision 要求,
// 所以,可以使用 '_ 来指定 NumRef 的生命周期,这时编译器推导为和 x 的生命周期一致。
//
// 如果省略 '_, 则 Rust 1.89.0 版本开始会发出编译警告。
fn as_num_ref(x: &i32) -> NumRef<'_> {
    NumRef { x: &x }
}

函数或方法的签名需要包含泛型参数类型的完整定义、约束,但对于需要 lifetime 参数的泛型类型,如 BufReader<'a>,1.89.0 版本之前是允许不指定 任何 lifetime 参数的。从 1.89.0 开始,如果函数返回值对象的类型定义包含 lifetime,则需要显式的使用 ‘_ 来让编译器推断,否则编译警告。

fn new1(buf: &mut [u8]) -> Thing<'_>;                 // elided - preferred
// 等效于:
fn new2(buf: &mut [u8]) -> Thing;                     // elided,Rust 1.89.0 版本开始会发出编译警告。
// 等效于:
fn new3<'a>(buf: &'a mut [u8]) -> Thing<'a>;

lifetime elision rule 可能带来的问题
#

// 自动推导的 &MyType2 的生命周期和 self 一致
fn get_token(&self, var: &MyType) -> Option<&MyType2> {}

// 但是如果要和 var 一致,则需要手动来指定 lifetime
fn get_token<'a, 'b>(&'a self, var: &'b MyType) -> Option<&'b MyType2> {}

由于 lifetime elision 的存在,有些需要 lifetime 的类型也可以不指定 lifetime,而由编译器推导:

// std::slice::Iter` 的定义包含一个 lifetime
pub struct Iter<'a, T>
where
    T: 'a,
{ /* private fields */ }

// 这里并没有为 Iter 类型的返回值指定 lifetime,而是由编译器自动推导
fn items(scores: &[u8]) -> std::slice::Iter<u8> {
   scores.iter()
}

从 Rust 1.89.0 开始,对于需要 lifetime 的类型,如果没有指定 ‘_ 或明确的 lifetime 标记,编译器会警告。

解决办法:显式的添加一个 ‘_ 标识,即明确告诉编译器按照 lifetime elision 规则进行推导,例如 fn items(scores: &[u8]) -> std::slice::Iter<'_, u8>

warning: hiding a lifetime that's elided elsewhere is confusing
 --> src/lib.rs:1:18
  |
1 | fn items(scores: &[u8]) -> std::slice::Iter<u8> {
  |                  ^^^^^     -------------------- the same lifetime is hidden here
  |                  |
  |                  the lifetime is elided here
  |
  = help: the same lifetime is referred to in inconsistent ways, making the signature confusing
  = note: `#[warn(mismatched_lifetime_syntaxes)]` on by default
help: use `'_` for type paths
  |
1 | fn items(scores: &[u8]) -> std::slice::Iter<'_, u8> {
  |                                             +++

其它可以消除 lifetime 的情况
#

Implied bounds:Lifetime bounds required for types to be well-formed are sometimes inferred.

参考:https://doc.rust-lang.org/reference/trait-bounds.html#implied-bounds

隐式推导(Implied bound)适用于函数的所有输入和输出参数,也适用于任意类型定义和 impl block。

// 隐式推导(Implied bound):T: 'a
fn requires_t_outlives_a<'a, T>(x: &'a T) {}

fn requires_t_outlives_a_not_implied<'a, T: 'a>() {}

// 隐式推导(Implied bound)会作用于函数的所有输入和输出参数
fn requires_t_outlives_a<'a, T>(x: &'a T) {
    // OK, 因为 &'a T 隐式表示 T: 'a, 满足函数签名要求
    requires_t_outlives_a_not_implied::<'a, T>();
}

fn not_implied<'a, T>() {
    // Error:因为 T 和 'a 之间的关系不明确
    requires_t_outlives_a_not_implied::<'a, T>();
}

lifetime 隐式推导作用于类型定义和 impl block 的例子:

struct Struct<'a, T> {
    // This requires `T: 'a` to be well-formed which is inferred by the compiler.
    field: &'a T,
}

enum Enum<'a, T> {
    // This requires `T: 'a` to be well-formed, which is inferred by the compiler.
    //
    // Note that `T: 'a` is required even when only using `Enum::OtherVariant`.
    SomeVariant(&'a T),
    OtherVariant,
}

trait Trait<'a, T: 'a> {}

// This would error because `T: 'a` is not implied by any type
// in the impl header.
//     impl<'a, T> Trait<'a, T> for () {}

// This compiles as `T: 'a` is implied by the self type `&'a T`.
impl<'a, T> Trait<'a, T> for &'a T {}

trait object 的缺省 lifetime 推导规则
#

参考:10-rust-lang-generic-trait.md

rust-lang - 这篇文章属于一个选集。
§ 6: 本文

相关文章

10. 泛型和特性:generic/trait
·
Rust
Rust 泛型和特性
5. 借用:refer/borrow
·
Rust
Rust 引用类型和借用
1. 标识符和注释:identify/comment
·
Rust
Rust 标识符介绍
11. 类型协变:type coercion
·
Rust
Rust 高级话题:子类型和类型协变