Rust Lifetime
Table of Contents
1. Rust Lifetime
1.1. dangling reference
lifetime 是用来解决 dangling reference 问题:
- 函数返回一个引用
- 成员函数返回一个引用
- 结构体中包含一个引用
这三种引用都有可能因为引用的资源被释放而变成 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, 它告诉编译器两个含义:
在编译 get_test 时, 返回值只能引用着 f1 , 或者是 'static
#[derive(Debug)] struct Test(); fn get_test<'a>(f1: &'a Test, f2: &Test) -> &'a Test { return f2; }
编译失败, 因为返回了 f2 的引用
在编译 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 时,
- 编译 get_test 时, f1, f2, 'static 都可以返回
- 编译 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 的规则:
函数的每个参数都分配一个 lifetime
fn test (f1 :&str, f2:&str) { } fn test2<'a, 'b> (f1 :&'a str, f2:&'b str) { }
若只有一个参数, 则所有输出的 lifetime 与参数相同
fn test (f1 :&str) -> &str { } fn test<'a> (f1 :&'a str) -> &'a str { }
- 若参数包含 &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. 总结
- 编译器不想(不能)自动计算出 lifetime
- lifetime 类似于函数的 signature
- 编译 test 时, 需要根据 lifetime annotation 确保输出引用着正确的参数
- 编译 main 时, 需要根据 lifetime annotation 确保被输出引用的参数有足够长的 lifetime