title: Lifetime tags: Rust basic wtfacademy WTF Rust 极简入门: 生命周期 在 Rust 中,生命周期是一个非常重要的概念,用于确保引用不会悬空,即引用的数据在引用存在的时间里始终有效。Rust 编译器通过生命周期来检查这种有效性。这一节将讨论生命周期的重要性、如何使用生命周期注解,以及它如何帮助我们写出更安全的代码。 悬空引用 在第 5 节我们知道,Rust 中所有的对象都是有主人(owner)的,它拥有对象的所有权(ownership),所有权具有唯一性,但很多时候我们并不需要对象的所有权,有使用权就够了,而使用权在 Rust 中称之为引用(borrow),或者说借用。
title: Lifetime tags: - Rust - basic - wtfacademy
在 Rust 中,生命周期是一个非常重要的概念,用于确保引用不会悬空,即引用的数据在引用存在的时间里始终有效。Rust 编译器通过生命周期来检查这种有效性。这一节将讨论生命周期的重要性、如何使用生命周期注解,以及它如何帮助我们写出更安全的代码。
在第 5 节我们知道,Rust 中所有的对象都是有主人(owner)的,它拥有对象的所有权(ownership),所有权具有唯一性,但很多时候我们并不需要对象的所有权,有使用权就够了,而使用权在 Rust 中称之为引用(borrow),或者说借用。
let s1 = String::from("hello"); let s2 = &s1;
在上面的例子中,s1 拥有 hello 这个对象的所有权,而 s2 通过 & 修饰符表示引用这个对象,默认是不可变引用。这是一个正常的例子,但在有些情况下,引用会变得无效:
let s2; { let s1 = String::from("hello"); s2 = &s1; } println!("s2: {}", s2);
上面的例子中,s2 引用的 s1 对象的作用域在花括号内部,超出这个作用域之后,s1 这个对象会被 Rust 自动回收,此时再去打印 s2 的值就会发生错误:s1 does not live long enough, borrowed value does not live long enough,即对象 s1 存活的时间不够长,导致 s2 引用了一个不存在的值,这种情况,我们称之为悬空引用。
Rust 中的每一个引用都有其生命周期(lifetime),也就是引用保持有效的作用域。生命周期在 Rust 中的核心作用是防止悬空引用的发生,它是许多程序错误和安全隐患的根源。生命周期确保内存安全,无需垃圾收集。
在大多数时候,我们无需手动的声明生命周期,因为编译器可以自动进行推导,但当多个生命周期存在时,编译器可能无法进行引用的生命周期分析,就需要我们手动标明不同引用之间的生命周期关系,也就是生命周期注解。
生命周期参数名称必须以撇号'开头,其名称通常全是小写,类似于泛型其名称非常短。'a 是默认使用的名称。生命周期参数标注位于引用符 & 之后,并有一个空格来将生命周期注解与引用的类型分隔开,如 &'a i32。
生命周期注解并不改变任何引用的生命周期的长短。它只是描述了多个引用生命周期相互的关系,便于编译器进行引用的分析,但不影响其生命周期。
生命周期即 Rust 中值的生老病死,而生命周期注解就是用于约定多个引用之间生死的关系,就像桃园三结义中誓约的那样:不求同年同月同日生,但求同年同月同日死,这三兄弟之间的誓约就如同 Rust 中的生命周期注解。誓约是为了防止有人背信弃义,而 Rust 生命周期注解是为了编译器进行分析,防止出现悬垂引用。有了誓约并不代表大家就一定一起赴死,就如同生命周期注解并不改变值的生命周期一样。
fn borrow<'a>(x: &'a i32, y: &'a i32) -> &'a i32 { if x > y { x } else { y } }
这个函数 borrow 接收两个引用参数,并返回一个它们中的一个。生命周期注解 'a 指示这两个输入引用和返回引用必须拥有相同的生命周期。
生命周期在结构体定义中尤其重要,尤其是当结构体要包含对某种数据的引用时。
struct Book<'a> { title: &'a str, pages: i32, } fn main() { let title = String::from("Rust Programming"); let book = Book { title: &title, pages: 384, }; println!("Book: {} - {} pages", book.title, book.pages); }
在这个例子中,Book 结构体有一个生命周期注解 'a,这意味着字段 title 的生命周期至少要和 Book 实例一样长,否则就会发生悬空引用。
在某些常见情况下,Rust 允许省略生命周期注解。编译器遵循一组特定的规则(称为生命周期省略规则),在这些规则适用的情况下,可以推断出引用的生命周期。
&self 或 &mut self(说明是方法),则 self 的生命周期被赋给所有输出生命周期参数。考虑到更复杂的场景,显式生命周期注解变得尤为重要。它能确保代码在引用和数据管理方面的正确性,特别是在多个不同生命周期和复杂数据类型交互时。
fn longest<'a, 'b: 'a>(x: &'a str, y: &'b str) -> &'a str { if x.len() > y.len() { x } else { y } }
这个函数 longest 涉及到了两个不同的生命周期注解 'a 和 'b,而 'b: 'a 表示输入参数 y 的生命周期至少与输入参数 x 相同,或比它更长。函数返回值中 'a 表示返回的引用将具有与输入参数 x 相同的生命周期,也就是生命周期中最小的那个。这样可能比较抽象,我们看下具体的例子:
fn main() { let string1 = String::from("abcdefghijklmnopqrstuvwxyz"); { let string2 = String::from("123456789"); let result = longest(string1.as_str(), string2.as_str()); println!("The longest string is {}", result); } }
上面的代码展示了 string1 和 string2 这两个不同生命周期的变量,前者的生命周期位于外部的 {} 中,而后者的生命周期位于内部的 {} 中,所以 'a 代表的生命周期范围是两者中较小的那个,即内部的 {},此时返回值 result 的生命周期也是属于内部的 {},即返回值能够保证在 string1 和 string2 中较短的那个生命周期结束前有效,此时不会发生悬垂引用,编译通过。
接下来,让我们尝试另外一个例子,该例子揭示了 result 的引用的生命周期必须是两个参数中较短的那个。
fn main() { let string1 = String::from("abcdefghijklmnopqrstuvwxyz"); let result; { let string2 = String::from("123456789"); result = longest(string1.as_str(), string2.as_str()); } println!("The longest string is {}", result); }
此时 'a 代表的生命周期范围依旧是变量 string1 和 string2 中最小的那个,即内部的 {},但返回值 result 的生命周期范围却是外部的 {},而不是内部的 {},也就意味着 result 可能会引用一个无效的值,因此编译失败。
注意:通过人为观察
result的引用应该为string1,这样返回值result和string1的作用域是一致的,理论上是应该编译通过的。但是,Rust 的编译器会采用保守的策略,我们通过生命周期标注告诉 Rust,longest函数返回值的生命周期是传入参数中较小的那个变量的生命周期,因此 Rust 编译器不允许上述代码通过,因为可能存在无效引用。
理解和正确使用生命周期是掌握 Rust 的重要部分。生命周期注解帮助 Rust 编译器保证引用的有效性,从而让你的程序在处理引用时更加安全。虽然开始时可能会觉得生命周期有些复杂,但随着实践的深入,你会逐渐领会它们的重要性和用法。掌握生命周期让你能写出更健壮、安全的 Rust 代码。如果你有任何问题或需要更多例子,请随时提问!