【Rust生命周期】一文搞懂Rust语言生命周期机制

在这里插入图片描述

✨✨ 欢迎大家来到景天科技苑✨✨

🎈🎈 养成好习惯,先赞后看哦~🎈🎈

🏆 作者简介:景天科技苑
🏆《头衔》:大厂架构师,华为云开发者社区专家博主,阿里云开发者社区专家博主,CSDN全栈领域优质创作者,掘金优秀博主,51CTO博客专家等。
🏆《博客》:Rust开发,Python全栈,Golang开发,云原生开发,PyQt5和Tkinter桌面开发,小程序开发,人工智能,js逆向,App逆向,网络系统安全,数据分析,Django,fastapi,flask等框架,云原生K8S,linux,shell脚本等实操经验,网站搭建,数据库等分享。

所属的专栏:Rust语言通关之路
景天的主页:景天科技苑

在这里插入图片描述

Rust生命周期

Rust语言以其内存安全和并发安全著称,而生命周期(Lifetimes)是实现这些安全特性的核心机制之一。生命周期是Rust中最具特色但也最令初学者困惑的概念之一。

一、生命周期基础

1.1 什么是生命周期

生命周期是Rust中用来确保引用始终有效的范围标记。它告诉编译器引用的有效作用域,从而防止悬垂引用(Dangling Reference)——即引用指向已经被释放的内存。
在Rust中,每个引用都有其生命周期,尽管大多数情况下编译器可以自动推断而不需要我们显式标注。
Rust 中的每一个引用都有其 生命周期(lifetime),也就是引用保持有效的作用域。大部分时候生命周期是隐含并可以推断的,正如大部分时候类型也是可以推断的一样。
类似于当因为有多种可能类型的时候必须注明类型,也会出现引用的生命周期以一些不同方式相关联的情况,
所以 Rust 需要我们使用泛型生命周期参数来注明他们的关系,这样就能确保运行时实际使用的引用绝对是有效的。

1.2 为什么需要生命周期

Rust 不使用垃圾回收,而是依赖所有权系统来管理内存。
为了保证在编译期就能检测出潜在的内存错误,Rust 要知道每个引用在内存中的作用域(scope)有多长,这就是生命周期的本质:一种描述引用有效范围的机制。
生命周期避免了悬垂引用

考虑以下代码:

fn main() {
    let r;
    {
        let x = 5;
        r = &x;
    }
    println!("r: {}", r);
}

这段代码无法编译,因为内部块中的x在块结束时被丢弃,而r试图在块外引用它。Rust的生命周期系统会捕获这种错误。
在这里插入图片描述

那么 Rust 是如何决定这段代码是不被允许的呢?

借用检查器
编译器的这一部分叫做 借用检查器(borrow checker),它比较作用域来确保所有的借用都是有效的。
在这里插入图片描述

我们将 r 的生命周期标记为 'a 并将 x 的生命周期标记为 'b 。如你所见,内部的 'b 块要比外部的生命周期 'a 小得多。
在编译时,Rust 比较这两个生命周期的大小,并发现 r 拥有生命周期 'a ,不过它引用了一个拥有生命周期’b 的对象。
程序被拒绝编译,因为生命周期 'b 比生命周期 'a 要小:被引用的对象比它的引用者存在的时间更短。

让我们看看示例 10-20 中这个并没有产生悬垂引用且可以正确编译的例子:
在这里插入图片描述

这里 x 拥有生命周期 'b ,比 'a 要大。这就意味着 r 可以引用 x :Rust 知道 r 中的引用在 x 有效的时候也总是有效的。
现在我们已经在一个具体的例子中展示了引用的生命周期位于何处,并讨论了 Rust 如何分析生命周期来保证引用总是有效的
rust的生命周期始终是和借用、引用相关的

1.3 生命周期注解语法

生命周期注解并不改变任何引用的生命周期的长短。与当函数签名中指定了泛型类型参数后就可以接受任何类型一样,
当指定了泛型生命周期后函数也能接受任何生命周期的引用。生命周期注解所做的就是将多个引用的生命周期联系起来。
生命周期参数以撇号’开头,通常是小写字母,如’a。
生命周期参数注解位于引用的 & 之后,并有一个空格来将引用类型与生命周期注解分隔开。

&i32        // 普通引用
&'a i32     // 带有显式生命周期的引用
&'a mut i32 // 带有显式生命周期的可变引用

二、函数中的生命周期

2.1 函数签名中的生命周期

就像泛型类型参数,泛型生命周期参数需要声明在函数名和参数列表间的尖括号中。fn funcname<'a> (x: &'a 参数类型, y: &'a 参数类型 )
这里我们想要告诉 Rust 关于参数中的引用和返回值之间的限制是他们都必须拥有相同的生命周期
当函数返回引用时,通常需要生命周期注解来说明返回的引用来自哪个参数。例如:

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

这里的’a是一个生命周期参数,表示x和y必须至少活得像’a一样长,且返回值也至少活得像’a一样长。
当在函数中使用生命周期注解时,这些注解出现在函数签名中,而不存在于函数体中的任何代码中。
这是因为 Rust 能够分析函数中代码而不需要任何协助,不过当函数引用或被函数之外的代码引用时,参数或返回值的生命周期可能在每次函数被调用时都不同。
这可能会产生惊人的消耗并且对于 Rust 来说通常是不可能分析的。在这种情况下,我们需要自己标注生命周期。

如果将函数中创建局部变量的引用返回,即便标注了生命周期,借用检查也会报错,因为函数内创建的局部变量,在离开函数作用域后,就会调用drop函数,将变量销毁
此时再返回该引用,会出现悬垂引用
在这里插入图片描述

出现的问题是 s 在 longest 函数的结尾将离开作用域并被清理,而我们尝试从函数返回一个 s 的引用。
无法指定生命周期参数来改变悬垂引用,而且 Rust 也不允许我们创建一个悬垂引用。
在这种情况,最好的解决方案是返回一个有所有权的数据类型而不是一个引用,这样函数调用者就需要负责清理这个值了。

从结果上看,生命周期语法是关于如何联系函数不同参数和返回值的生命周期的。
一旦他们形成了某种联系,Rust 就有了足够的信息来允许内存安全的操作并阻止会产生悬垂指针亦或是违反内存安全的行为。

2.2 实际应用示例

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}
fn main() {
    let string1 = String::from("long string is long");

    {
        let string2 = String::from("xyz");
        let result = longest(string1.as_str(), string2.as_str());
        println!("The longest string is {}", result);
    }
}

在这里插入图片描述

这个例子可以正常工作,因为string2的生命周期足够长。但如果尝试在string2离开作用域后使用result,编译器会报错。
在这里插入图片描述

2.3 生命周期省略规则

早期在Rust中,必须显示地声明生命周期,后来将很明确的模式进行了注解的简化。
Rust团队发现某些生命周期模式非常常见,因此编译器可以自动推断这些模式而不需要显式注解。
遵守生命周期省略规则的情况下能明确变量的声明周期,则无需明确指定生命周期。函数或者方法的参数的生命周期称为输入生命周期,而返回值的生命周期称为输出生命周期。
编译器采用三条规则判断引用何时不需要生命周期注解,当编译器检查完这三条规则后仍然不能计算出引用的生命周期,则会停止并生成错误。

这些规则称为"生命周期省略规则":
1)每个引用参数都有自己的生命周期参数,换句话说就是,有一个引用参数的函数有一个生命周期参数,有两个引用参数的函数有两个不同的生命周期参数

fn foo(x: &i32)fn foo<'a>(x: &'a i32)
fn foo(x: &i32, y: &i32)fn foo<'a, 'b>(x: &'a i32, y: &'b i32)

2)如果只有一个输入生命周期参数,它被赋予所有输出生命周期参数

fn foo(x: &i32) -> &i32fn foo<'a>(x: &'a i32) -> &'a i32

3)方法中如果有多个输入生命周期参数,但其中一个是&self或&mut self,则self的生命周期被赋予所有输出生命周期参数。这使得方法编写起来更简洁。

三、结构体中的生命周期

3.1 结构体定义中的生命周期

目前为止,我们只定义过有所有权类型的结构体。也可以定义存放引用的结构体,不过需要为结构体定义中的每一个引用添加生命周期注解
当结构体包含引用时,必须在结构体定义中声明生命周期:

//结构体中的生命周期
// 结构体中包含一个引用
// 结构体的生命周期必须与引用的生命周期相同
//也是以尖括号的形式来表示生命周期
struct ImportantExcerpt<'a> {
    part: &'a str,
}

fn main() {
    let novel = String::from("Call me jingtian. Some years ago...");
    //split('.')‌:这个方法将调用者字符串在每个点号处分割,返回一个Split迭代器
    //next()‌:这个方法用于获取迭代器中的下一个元素。如果迭代器为空,则返回None
    //expect()‌:这个方法用于获取Option类型的值,如果是None,则会触发panic,并输出指定的错误信息
    let first_sentence = novel.split('.').next().expect("Could not find a '.'");
    let i = ImportantExcerpt {
        part: first_sentence,
    };
    println!("The first sentence is: {}", i.part);
    // 这里的 i.part 的生命周期与 novel 的生命周期相同
    // 因为 novel 在这里仍然有效,所以 i.part 也有效
    // 但是如果 novel 的生命周期结束了,i.part 就会变得无效
}

在这里插入图片描述

3.2 实际案例:文本处理

考虑一个文本处理应用,我们需要高效地引用原始文本的某部分而不进行拷贝:

//结构体中的生命周期
struct TextSpan<'a> {
    content: &'a str,
    start: usize,
    end: usize,
}

//实现结构体的方法
// 这里的 'a 是一个生命周期参数,表示 content 的生命周期
// 它必须与 TextSpan 结构体的生命周期相同
// 这意味着 TextSpan 结构体不能比 content 的生命周期更长
// 也就是说,TextSpan 结构体的生命周期不能超过 content 的生命周期
impl<'a> TextSpan<'a> {
    //关联函数
    // 传参文本字面量引用,起始位置和结束位置
    // 返回一个 TextSpan 实例
    fn new(content: &'a str, start: usize, end: usize) -> Self {
        Self {
            content: &content[start..end],
            start,
            end,
        }
    }

    fn display(&self) {
        println!("Span: '{}' ({}..{})", self.content, self.start, self.end);
    }
}

fn main() {
    let text = "The quick brown fox jumps over the lazy dog.";
    //截取字符串
    //截取坐标16到19的字符串
    let fox_span = TextSpan::new(text, 16, 19);
    fox_span.display();
}

在这里插入图片描述

四、方法定义中的生命周期

4.1 impl块中的生命周期

在为带有生命周期的结构体实现方法时,需要在impl后声明生命周期:

//结构体中的生命周期
// 结构体中包含一个引用
// 结构体的生命周期必须与引用的生命周期相同
//也是以尖括号的形式来表示生命周期
struct ImportantExcerpt<'a> {
    part: &'a str,
}

//方法中的生命周期impl<'a>标注  结构体后面也要标注
impl<'a> ImportantExcerpt<'a> {
    //这里只有一个输入生命周期参数,参数是 &self ,返回值类型被赋予了 &self 的生命周期,这样所有的生命周期都被计算出来了。
    fn level(&self) -> i32 {
        3
    }
    fn level2(&self) -> &str {
        //等价于 fn level2(&'a self) -> &'a str {    方法名后面不用标注生命周期
        self.part
    }


    //方法中的生命周期
    //如果有多个输入生命周期参数,但其中一个是&self或&mut self,则self的生命周期被赋予所有输出生命周期参数。可以省略生命周期的标注。这使得方法编写起来更简洁。
    fn announce_and_return_part(&self, announcement: &str) -> &str {
        println!("Attention please: {}", announcement);
        self.part
    }
}


fn main() {
    let novel = String::from("Call me jingtian. Some years ago...");
    //split('.')‌:这个方法将调用者字符串在每个点号处分割,返回一个Split迭代器
    //next()‌:这个方法用于获取迭代器中的下一个元素。如果迭代器为空,则返回None
    //expect()‌:这个方法用于获取Option类型的值,如果是None,则会触发panic,并输出指定的错误信息
    let first_sentence = novel.split('.').next().expect("Could not find a '.'");
    //创建一个结构体实例
    let i = ImportantExcerpt {
        part: first_sentence,
    };
    println!("The first sentence is: {}", i.part);
    // 这里的 i.part 的生命周期与 novel 的生命周期相同
    // 因为 novel 在这里仍然有效,所以 i.part 也有效
    // 但是如果 novel 的生命周期结束了,i.part 就会变得无效

    //实例对象调用方法
    let part = i.announce_and_return_part("Hello, world!");
    println!("The part is: {}", part);
    let a = i.level();
    println!("The level is: {}", a);
}

在这里插入图片描述

但是,如果方法返回的是个不同于self的引用,则方法也应该标注生命周期,因为编译器推导默认是self的生命周期,此时无法推导出来
如果不标注,就会报错
在这里插入图片描述

正确标注方法:

//包含多个参数的方法
//不标注的情况下,默认是 &self 的生命周期
//如果返回的是一个不是和self相关的引用,必须标注生命周期
//首先需要在方法处生命生命周期参数,然后在入参和返回参数处标注
fn level3<'b>(&self, c: &'b str) -> &'b str {
    c
}

在这里插入图片描述

4.2 生命周期与self的关系

在方法中,self的生命周期会应用于返回的引用(根据省略规则3)。例如上面的announce_and_return_part方法,编译器会自动应用生命周期。

五、静态生命周期

5.1 'static生命周期

'static是一个特殊的生命周期,表示整个程序的持续时间。所有字符串字面量都有’static生命周期:

let s: &'static str = "I have a static lifetime.";

字符串字面值有 'static 生命周期
全局变量通常有 'static 生命周期
静态数据通常有 'static 生命周期
谨慎使用,通常不需要指定 'static
'static 也通常用于线程生命周期绑定

但是在局部作用域中定义,出了作用域还是被释放
在这里插入图片描述

5.2 使用场景与注意事项

虽然’static看起来很方便,但应该谨慎使用。真正的’static数据会一直存在于程序的整个生命周期中,不会被释放。

六、生命周期与泛型结合

6.1 泛型类型参数、trait约束和生命周期

可以同时使用生命周期和泛型类型参数:

use std::fmt::Display;

//函数中生命周期和泛型参数的使用

fn longest_with_an_announcement<'a, T>(x: &'a str, y: &'a str, ann: T) -> &'a str where T: Display {
    println!("Announcement! {}", ann);
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

fn main() {
    let string1 = String::from("long string is long");
    let string2 = String::from("xyz");
    let result = longest_with_an_announcement(string1.as_str(), string2.as_str(), "Hello, world!");
    println!("The longest string is {}", result);
}

在这里插入图片描述

这个是那个返回两个字符串 slice 中较长者的 longest 函数,不过带有一个额外的参数 ann 。
ann 的类型是泛型 T ,它可以被放入任何实现了 where 从句中指定的 Display trait 的类型。
这个额外的参数会在函数比较字符串slice 的长度之前被打印出来,这也就是为什么 Display trait bound 是必须的。
因为生命周期也是泛型,所以生命周期参数 'a 和泛型类型参数 T 都位于函数名后的同一尖括号列表中。

6.2 实际案例:缓存系统

//缓存系统
//泛型结构体,约束为可克隆的引用类型
//生命周期约束

struct Cache<'a, T> where T: 'a + Clone {
    data: &'a T,
    timestamp: u64,
}

//实现结构体的方法
impl<'a, T> Cache<'a, T> where T: 'a + Clone {
    fn new(data: &'a T, timestamp: u64) -> Self {
        Self { data, timestamp }
    }

    fn get_data(&self) -> T {
        self.data.clone()
    }

    fn is_expired(&self, current_time: u64) -> bool {
        current_time > self.timestamp + 3600
    }
}

fn main() {
    //创建缓存
    let important_data = String::from("Critical data");
    let cache = Cache::new(&important_data, 1234567890);

    //使用缓存
    println!("Cached data: {}", cache.get_data());
    //检查缓存是否过期
    println!("Is expired: {}", cache.is_expired(1234768000));
}

在这里插入图片描述

七、高级生命周期模式

7.1 生命周期子类型

有时需要表达一个生命周期比另一个更长。这称为生命周期子类型:

//定义生命周期长短
fn longest_with_elision<'a, 'b: 'a>(x: &'a str, y: &'b str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}
fn main() {
    let a = "a";
    let b = "b";
    let c = longest_with_elision(a, b);
    println!("c: {}", c);
}

这里的’b: 'a表示’b至少和’a一样长。
在这里插入图片描述

八、常见生命周期错误与解决方法

8.1 错误示例1:返回局部变量的引用

fn invalid_reference() -> &str {
    let s = String::from("hello");
    &s
}

解决方法:返回字符串本身而不是引用,或者让调用方提供存储空间。

8.2 错误示例2:结构体生命周期不匹配

struct Holder<'a> {
    data: &'a str,
}

fn create_holder() -> Holder {
    let s = String::from("temp");
    Holder { data: &s }
}

解决方法:确保被引用的数据比结构体活得长,或者使用所有权而非引用。

8.3 错误示例3:迭代器与生命周期的交互

fn first_word<'a>(words: &'a Vec<String>) -> &'a str {
    words.iter().next().unwrap()
}

问题:看起来没问题,但可能在实际使用中遇到生命周期不够长的情况。
解决方法:明确生命周期约束或重新设计API。

九、总结

Rust的生命周期系统是它内存安全保证的核心部分。虽然初学时有陡峭的学习曲线,但一旦掌握,它能够帮助你编写出既安全又高效的代码。
通过本文的理论讲解和实际案例,希望大家已经对生命周期有了深入的理解。记住,实践是掌握生命周期的关键——多写代码,多与编译器"对话",你会逐渐培养出对生命周期的直觉。

评论 42
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

景天科技苑

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值