Rust Lifetime

Table of Contents

1. Rust Lifetime

1.1. dangling reference

lifetime 是用来解决 dangling reference 问题:

  1. 函数返回一个引用
  2. 成员函数返回一个引用
  3. 结构体中包含一个引用

这三种引用都有可能因为引用的资源被释放而变成 dangling reference

1.2. 为什么需要提供 lifetime annotation

#[derive(Debug)]
struct Test();

fn get_test(f1: &Test, f2: &Test) -> &Test {
    return f1;
}

static g_test: Test = Test();
fn main() {
    let f1 = Test();
    get_test(&f1, &g_test);

    let f;
    {
        let f1 = Test();
        f = get_test(&f1, &g_test);
    }
    println!("{:?}", f);
}

对于上面的代码, 若不考虑 lifetime, 则第一个 get_test 有正确的, 而第二个是错误的, 会导致 f 变成 dangling reference.

rust 编译器在编译 main 时, 需要深入到 get_test 的 `实现` 才能确定第二个 get_test 是非法的, 对编译器可能有些困难. 另外, 若 get_test 是以 lib 或 ffi 方式提供, 则编译器根本不可能完成这个任务.

因此 rust 要求 get_test 把 ` get_test 会返回 f1 的引用` 的信息通过 lifetime 的方式提供给编译器, 而不是依赖编译器自己分析.

#[derive(Debug)]
struct Test();

fn get_test<'a>(f1: &'a Test, f2: &Test) -> &'a Test {
    return f1;
}

static g_test: Test = Test();
fn main() {
    let f1 = Test();
    get_test(&f1, &g_test);

    let f;
    {
        let f1 = Test();
        f = get_test(&f1, &g_test);
    }
    println!("{:?}", f);
}

有了 lifetime 信息, 编译器会发现第二个 get_test 是非法的

1.3. lifetime annotation 如何解读

fn get_test<'a>(f1: &'a Test, f2: &Test) -> &'a Test {
    return f1;
}

`'a` 是一个 lifetime annotation, 它告诉编译器两个含义:

  1. 在编译 get_test 时, 返回值只能引用着 f1 , 或者是 'static

    #[derive(Debug)]
    struct Test();
    
    fn get_test<'a>(f1: &'a Test, f2: &Test) -> &'a Test {
        return f2;
    }
    

    编译失败, 因为返回了 f2 的引用

  2. 在编译 get_test 的调用者时, 返回值的 lifetime 需要 <= f1, 因为它 `可能` 引用着 f1

    #[derive(Debug)]
    struct Test();
    
    fn get_test<'a>(f1: &'a Test, f2: &Test) -> &'a Test {
        return f1;
    }
    
    static g_test: Test = Test();
    fn main() {
        let f;
        {
            let f1 = Test();
            f = get_test(&f1, &g_test);
        }
        println!("{:?}", f);
    }
    

    编译失败, 因为 f 的 lifetime 比 f1 小.

    #[derive(Debug)]
    struct Test();
    
    static g_test: Test = Test();
    
    fn get_test<'a>(f1: &'a Test, f2: &Test) -> &'a Test {
        return &g_test;
    }
    
    fn main() {
        let f;
        {
            let f1 = Test();
            f = get_test(&f1, &g_test);
        }
        println!("{:?}", f);
    }
    

    虽然 get_test 返回的是 g_test, 但编译 main 时仍然会失败, 因为编译器在编译 main 时只看函数的 lifetime annotation

1.4. 多个 lifetime

#[derive(Debug)]
struct Test();

static g_test: Test = Test();

fn get_test<'a>(f1: &'a Test, f2: &'a Test) -> &'a Test {
    if true {
        return f1;
    }
    return f2;
}

fn main() {
    let f;
    {
        let f1 = Test();
        f = get_test(&f1, &g_test);
    }
    println!("{:?}", f);
}

f1, f2 有相同的 lifetime annotation 时,

  1. 编译 get_test 时, f1, f2, 'static 都可以返回
  2. 编译 main 时, 返回值的 lifetime 需要 <= min(f1,f2)

1.5. struct 的 lifetime

struct Test<'a> {
    s: &'a str,
}

编译 struct Test 相关的代码时, Test lifetime 需要比 s 小.

#[derive(Debug)]
struct Test<'a> {
    s: &'a str,
}

fn main() {
    let test;
    {
        test = Test {
            s: &"hello".to_owned(),
        }
    }
    println!("{:?}", test);
}

编译失败, 因为 Test 的 lifetime 大于 s

1.6. method 的 lifetime

method 的 lifetime 与 函数的 lifetime 类似, 但由于 method 可以引用 &self 的成员, 所以需要指定一个 annotation 表示 &self

#[derive(Debug)]
struct Test<'a> {
    s: &'a str,
}

// 'x 代表 Test 的 lifetime
impl<'x> Test<'x> {
    // method 的 'x 表示 Test 的 lifetime
    fn get_str(&'x self) -> &'x str {
        // return &"hello".to_owned(); 会失败
        return self.s;
    }
}

fn main() {
    let test = Test { s: "hello" };
    // 编译 main 时需要确保 s 的 lifetime <= test
    let s = test.get_str();
    println!("{:?}", s);

    // 编译失败, 因为返回值的 lifetime > test
    // let s;
    // {
    // let test = Test { s: "hello" };
    // s = test.get_str();
    // }
    // println!("{:?}", s);
}

1.7. lifetime elision

elision rule 是针对 fn 和 impl 的规则:

  1. 函数的每个参数都分配一个 lifetime

    fn test (f1 :&str, f2:&str)  {
    
    }
    
    fn test2<'a, 'b> (f1 :&'a str, f2:&'b str)  {
    
    }
    
  2. 若只有一个参数, 则所有输出的 lifetime 与参数相同

    fn test (f1 :&str) -> &str {
    
    }
    
    fn test<'a> (f1 :&'a str) -> &'a str {
    
    }
    
  3. 若参数包含 &self, 则所有输出的 lifetime 与 self 相同

有时 lifetime elision 并不是我们想要的:

#[derive(Debug)]
struct Test();

static g_test: Test = Test();

fn get_test(f1: &Test) -> &Test {
    return &g_test;
}

fn main() {
    let f;
    {
        let f1 = Test();
        f = get_test(&f1);
    }
    println!("{:?}", f);
}

get_test 根据 elision 规则, 等价于:

fn get_test<'a>(f1: &'a Test) -> &'a Test {
    return &g_test;
}

编译 get_test 时没有问题, 但编译 main 时出错, 因为 f > f1, 这里我们需要修改 get_test 为:

#[derive(Debug)]
struct Test();

static g_test: Test = Test();

fn get_test(f1: &Test) -> &'static Test {
    return &g_test;
}

fn main() {
    let f;
    {
        let f1 = Test();
        f = get_test(&f1);
    }
    println!("{:?}", f);
}

1.8. 总结

  1. 编译器不想(不能)自动计算出 lifetime
  2. lifetime 类似于函数的 signature
  3. 编译 test 时, 需要根据 lifetime annotation 确保输出引用着正确的参数
  4. 编译 main 时, 需要根据 lifetime annotation 确保被输出引用的参数有足够长的 lifetime

Author: [email protected]
Date: 2018-12-27 Thu 00:00
Last updated: 2024-08-05 Mon 17:51

知识共享许可协议