简而言之,即引用的有效作用域;一般情况下编译器会自动检查推导,但是当多个声明周期存在时,编译器无法推导出某个引用的生命周期,需要手动标明生命周期。

悬垂指针

悬垂指针是指一个指针指向了被释放的内存或者没有被初始化的内存。当尝试使用一个悬垂指针时,无法保证内存中的数据是否有效,从而导致程序的不确定行为。

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

问题:r 引用了内部花括号中的 x 变量,但是 x 会在内部花括号 } 处被释放,因此回到外部花括号后,r 会引用一个无效的 x。此时 r 就是一个悬垂指针,它引用了提前被释放的变量 x。生命周期就是为了解决这种问题的出现。

借用检查

Rust 使用借用检查器检查程序的借用正确性。

{
let r; // ---------+-- 'a
// |
{ // |
let x = 5; // -+-- 'b |
r = &x; // | |
} // -+ |
// |
println!("r: {}", r); // |
} // ---------+

r 变量被赋予了生命周期 'a ,x 被赋予了生命周期 'b 。在编译期,Rust 会比较两个变量的生命周期,发现 r 拥有生命周期 'a,但是却引用了一个小得多的生命周期 'b,在这种情况下,编译器会认为程序存在风险拒绝运行。

如果想编译通过,需要使 x 变了活得比 r 久

{
let x = 5; // ----------+-- 'b
// |
let r = &x; // --+-- 'a |
// | |
println!("r: {}", r); // | |
// --+ |
} // ----------+

生命周期

定义及语法

Rust 生命周期通过引用和所有权机制来管理内存的使用。每个变量都有其所有权,只有所有者可以释放其对应的内存。当变量被移动或引用时,其所有权也会随之转移。

需要注意的是,生命周期标注不会改变引用的实际作用域。编译器无法确定生命周期时,需要人工明确指定变量和引用的生命周期,以确保它们的使用是安全和有效的。

生命周期的语法:以 ' 开头,名称常用一个单独的小写字母如 'a 。

fn useless<'a>(first: &'a i32, second: &'a i32) {}

如果是引用类型的参数,那么生命周期会位于引用符号 & 之后,并用一个空格来将生命周期和引用参数分隔开:

&i32        // 一个引用
&'a i32 // 具有显式生命周期的引用
&'a mut i32 // 具有显式生命周期的可变引用

函数中的生命周期

原始函数如下

fn main() {
let string1 = String::from("abcd");
let string2 = "xyz";
let result = longest(string1.as_str(), string2);
println!("The longest string is {}", result);
} fn longest(x: &str, y: &str) -> &str {
if x.len() > y.len() {
x
} else {
y
}
}

运行时会抛出异常

error[E0106]: missing lifetime specifier
--> main.rs:8:33
|
8 | fn longest(x: &str, y: &str) -> &str {
| ---- ---- ^ expected named lifetime parameter
|
= help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `x` or `y`
help: consider introducing a named lifetime parameter
|
8 | fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
| ++++ ++ ++ ++ error: aborting due to previous error

由异常可以看到,编译器无法确定 longest 函数的返回值是引用 x 还是引用 y 来进行后续的引用生命周期分析。即在存在多个引用时,编译器有时会无法自动推导生命周期

对 longest 函数添加生命周期标注

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
  • 和泛型一样,使用生命周期参数,需要先声明 <'a>
  • x、y 和返回值至少活得和 'a 一样久

需要强调的是,在通过函数签名指定生命周期参数时没有改变传入引用或者返回引用的真实生命周期,而是告诉编译器当不满足此约束条件时,就拒绝编译通过

当把具体的引用传给 longest 时,那生命周期 'a 的大小就是 x 和 y 的作用域的重合部分,即'a 大小为 x 和 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 的生命周期比 string1 小,因此 result 和 string2 活的一样久。如果对代码进行修改

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

此时由于生命周期 result = string1 > string2,因此会抛出异常

 --> main.rs:6:44
|
5 | let string2 = String::from("xyz");
| ------- binding `string2` declared here
6 | result = longest(string1.as_str(), string2.as_str());
| ^^^^^^^^^^^^^^^^ borrowed value does not live long enough
7 | }
| - `string2` dropped here while still borrowed
8 | println!("The longest string is {}", result);
| ------ borrow later used here error: aborting due to previous error

特殊场景

只返回第一个参数

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

y 完全没有被使用,即不需要为 y 标注生命周期,只需要标注 x 参数和返回值即可。

返回值为引用

若返回值为引用类型,则生命周期为

  • 函数参数的生命周期
  • 函数体中某个新建引用的生命周期

若为后者有可能出现悬垂指针:

fn longest<'a>(x: &str, y: &str) -> &'a str {
let result = String::from("really long string");
result.as_str()
}

此时抛出异常

error[E0515]: cannot return reference to local variable `result`
--> main.rs:15:5
|
15 | result.as_str()
| ^^^^^^^^^^^^^^^ returns a reference to data owned by the current function

result 在函数结束后就被释放,但是仍然存在对 result 的引用,无法指定合适的生命周期(好处是避免了悬垂引用),为了解决这种异常,可以返回内部字符串的所有权,将字符串的所有权转移给调用者。

fn longest<'a>(_x: &str, _y: &str) -> String { //此处直接返回的是String,而不是引用类型
String::from("really long string")
}
fn main() {
let s = longest("not", "important");
println!("{}",s)
}

结构体中的生命周期

在结构体的字段中存在引用时,需要为结构体的每个引用标注生命周期。

#[allow(dead_code)]
#[derive(Debug)]
struct ImportantExcerpt<'a> {
part: &'a str,
}
fn main() {
let novel = String::from("Call me Ishmael. Some years ago...");
let first_sentence = novel.split('.').next().expect("Could not find a '.'");
let i = ImportantExcerpt {
part: first_sentence,
};
println!("{:?}",i);
}

结构体的生命周期标注语法跟泛型参数语法很像,需要对生命周期参数进行声明 <'a>。该生命周期标注说明,结构体 ImportantExcerpt 所引用的字符串 str 必须比该结构体活得更久。

#[allow(dead_code)]
#[derive(Debug)]
struct ImportantExcerpt<'a> {
part: &'a str,
}
fn main() {
let i;
{
let novel = String::from("Call me Ishmael. Some years ago...");
let first_sentence = novel.split('.').next().expect("Could not find a '.'");
i = ImportantExcerpt {
part: first_sentence,
};
}
println!("{:?}",i); // 此处使用了结构体,而里面引用的字符串
}

引用字符串在内部语句块末尾 } 被释放后,println! 依然在外面使用了该结构体,因此会导致无效的引用

error[E0597]: `novel` does not live long enough
--> main.rs:10:30
|
9 | let novel = String::from("Call me Ishmael. Some years ago...");
| ----- binding `novel` declared here
10 | let first_sentence = novel.split('.').next().expect("Could not find a '.'");
| ^^^^^^^^^^^^^^^^ borrowed value does not live long enough
...
14 | }
| - `novel` dropped here while still borrowed
15 | println!("{:?}",i);
| - borrow later used here

生命周期的消除规则

对于编译器来说,每一个引用类型都有一个生命周期,但是在编写程序时无需标注

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[..]
}

编译器为了简化用户的使用,运用了生命周期消除。对于 first_word 函数,它的返回值是一个引用类型,那么该引用只有两种情况:

  • 从参数获取
  • 从函数体内部新创建的变量获取

当返回值的引用是来自参数时,说明参数和返回值的生命周期是一样的。因此不标注也不会产生歧义。

消除规则

注意点:

  • 若编译器不能确定某件事是正确时,会直接判为不正确,此时需要手动标注生命周期
  • 函数或者方法中,参数的生命周期被称为输入生命周期,返回值的生命周期被称为输出生命周期

三条消除规则

  • 每一个引用参数都会获得独自的生命周期(输入)
  • 若只有一个输入生命周期(函数参数中只有一个引用类型),该生命周期会被赋给所有的输出生命周期,也就是所有返回值的生命周期都等于该输入生命周期(输出)
  • 若存在多个输入生命周期,且其中一个是 &self 或 &mut self,则 &self 的生命周期被赋给所有的输出生命周期(输出)

    注:存在&self 说明该函数是一个方法。如果返回值生命周期和 &self 不一样需要手动标注。

方法中的生命周期

为具有生命周期的结构体实现方法时,使用的语法跟泛型参数语法很相似

struct ImportantExcerpt<'a> {
part: &'a str,
}
impl<'a> ImportantExcerpt<'a> {
fn level(&self) -> i32 {
3
}
}

注意点:

  • impl 中必须使用结构体的完整名称,包括 <'a>,因为生命周期标注也是结构体类型的一部分
  • 方法签名中,由于生命周期消除的第一和第三规则一般不需要标注生命周期

编译器应用规则步骤:

原始代码

impl<'a> ImportantExcerpt<'a> {
fn announce_and_return_part(&self, announcement: &str) -> &str {
println!("Attention please: {}", announcement);
self.part
}
}

编译器应用第一规则,给予每个输入参数一个生命周期

impl<'a> ImportantExcerpt<'a> {
fn announce_and_return_part<'b>(&'a self, announcement: &'b str) -> &str { //不知道announcement生命周期,因此重新赋予生命周期b
println!("Attention please: {}", announcement);
self.part
}
}

编译器应用第三规则,将 &self 的生命周期赋给返回值 &str

impl<'a> ImportantExcerpt<'a> {
fn announce_and_return_part<'b>(&'a self, announcement: &'b str) -> &'a str {
println!("Attention please: {}", announcement);
self.part
}
}

如果手动标注返回值的生命周期为 'b,此时会报错,这是由于编译器无法知道 'a 和 'b 的关系。由于 &'a self 是被引用的一方,因此引用它的 &'b str 必须要活得比它短,否则会出现悬垂引用,因此需要使生命周期 'b 必须要比 'a 小。

方法 1:生命周期约束语法'a: 'b,说明'a 必须比 'b 活得久;把 'a 和 'b 都在同一个地方声明

impl<'a: 'b, 'b> ImportantExcerpt<'a> {
fn announce_and_return_part(&'a self, announcement: &'b str) -> &'b str {
println!("Attention please: {}", announcement);
self.part
}
}

方法 2:分开声明但通过 where 'a: 'b 约束生命周期关系

impl<'a> ImportantExcerpt<'a> {
fn announce_and_return_part<'b>(&'a self, announcement: &'b str) -> &'b str
where
'a: 'b,
{
println!("Attention please: {}", announcement);
self.part
}
}

静态生命周期

静态生命周期'static,拥有该生命周期的引用可以和整个程序活得一样久。

let s: &'static str = "long static";

可以帮助解决非常复杂的生命周期问题甚至是无法被手动解决的生命周期问题

'static,有两种用法: &'static 和 T: 'static

&'static

一个引用必须要活得跟剩下的程序一样久,才能被标注为 &'static。主要注意的是, &'static 生命周期针对的仅仅是引用,而不是持有该引用的变量,对于变量来说,还是要遵循相应的作用域规则:

use std::{slice::from_raw_parts, str::from_utf8_unchecked};

fn get_memory_location() -> (usize, usize) {
// “Hello World” 是字符串字面量,因此它的生命周期是 `'static`.
// 但持有它的变量 `string` 的生命周期就不一样了,它完全取决于变量作用域,对于该例子来说,也就是当前的函数范围
let string = "Hello World!";
let pointer = string.as_ptr() as usize;
let length = string.len();
(pointer, length)
// `string` 在这里被 drop 释放
// 虽然变量被释放,无法再被访问,但是数据依然还会继续存活
} fn get_str_at_location(pointer: usize, length: usize) -> &'static str {
// 使用裸指针需要 `unsafe{}` 语句块
unsafe { from_utf8_unchecked(from_raw_parts(pointer as *const u8, length)) }
} fn main() {
let (pointer, length) = get_memory_location();
let message = get_str_at_location(pointer, length);
println!(
"The {} bytes at 0x{:X} stored: {}",
length, pointer, message //The 12 bytes at 0x563D1B68305B stored: Hello World!
);
}

可以看到持有 &'static 引用的变量的生命周期受到作用域的限制

T: 'static

use std::fmt::Debug;

fn print_it<T: Debug + 'static>( input: T) {
println!( "'static value passed in is: {:?}", input );
} fn print_it1( input: impl Debug + 'static ) {
println!( "'static value passed in is: {:?}", input );
} fn main() {
let i = 5;
print_it(&i);
print_it1(&i);
}

上述两种形式的代码中,T: 'static 与 &'static 有相同的约束:T 必须活得和程序一样久;该代码会报错,&i 的生命周期无法满足 'static 的约束

修改方法一:将 i 修改为常量

fn main() {
const I: i32 = 5;
print_it(&I);
}

修改方法二: 将 T 修改为 &T

use std::fmt::Debug;

fn print_it<T: Debug + 'static>( input: &T) {
println!( "'static value passed in is: {:?}", input );
} fn main() {
let i = 5;
print_it(&i);
}

原因在于约束的是 T,但是使用是它的引用 &T,即没有直接使用 T,因此不会检查 T 的生命周期约束。

总而言之,&'static 引用指向的数据活得跟程序一样久,引用本身是要遵循其作用域范围的。

Rust——生命周期的更多相关文章

  1. Rust生命周期bound用于泛型的引用

    在实际编程中,可能会出现泛型引用这种情况,我们会编写如下的代码: struct Inner<'a, T> { data: &'a T, } 会产生编译错误: error[E0309 ...

  2. rust 生命周期2

    之前定义的结构体,都是不含引用的. 如果想定义含引用的结构体,请定义生命周期注解 #[warn(unused_variables)] struct ImportantExcerpt<'a> ...

  3. rust 函数-生命周期

    记录一下自己理解的生命周期. 每个变量都有自己的生命周期. 在c++里生命周期好比作用域, 小的作用域的可以使用大作用域的变量. 如果把这里的每个作用域取个名,那么就相当于rust里的生命周期注解. ...

  4. 【译】深入理解Rust中的生命周期

    原文标题:Understanding Rust Lifetimes 原文链接:https://medium.com/nearprotocol/understanding-rust-lifetimes- ...

  5. 对Rust所有权、借用及生命周期的理解

    Rust的内存管理中涉及所有权.借用与生命周期这三个概念,下面是个人的一点粗浅理解. 一.从内存安全的角度理解Rust中的所有权.借用.生命周期 要理解这三个概念,你首要想的是这么做的出发点是什么-- ...

  6. 文盘Rust -- struct 中的生命周期

    最近在用rust 写一个redis的数据校验工具.redis-rs中具备 redis::ConnectionLike trait,借助它可以较好的来抽象校验过程.在开发中,不免要定义struct 中的 ...

  7. [易学易懂系列|rustlang语言|零基础|快速入门|(5)|生命周期Lifetime]

    [易学易懂系列|rustlang语言|零基础|快速入门|(5)] Lifetimes 我们继续谈谈生命周期(lifttime),我们还是拿代码来说话: fn main() { let mut a = ...

  8. react组件的生命周期

    写在前面: 阅读了多遍文章之后,自己总结了一个.一遍加强记忆,和日后回顾. 一.实例化(初始化) var Button = React.createClass({ getInitialState: f ...

  9. 浅谈 Fragment 生命周期

    版权声明:本文为博主原创文章,未经博主允许不得转载. 微博:厉圣杰 源码:AndroidDemo/Fragment 文中如有纰漏,欢迎大家留言指出. Fragment 是在 Android 3.0 中 ...

  10. C# MVC 5 - 生命周期(应用程序生命周期&请求生命周期)

    本文是根据网上的文章总结的. 1.介绍 本文讨论ASP.Net MVC框架MVC的请求生命周期. MVC有两个生命周期,一为应用程序生命周期,二为请求生命周期. 2.应用程序生命周期 应用程序生命周期 ...

随机推荐

  1. linux基本文件命令复习笔记

    https://www.bilibili.com/video/BV1ex411x7Em/?p=4&spm_id_from=pageDriver&vd_source=92305fa48e ...

  2. [C++] 进程注入dll版

    目录 前言 前提 原理 代码 一些问题 前言 这几天在弄进程注入的事情,一直做不出来直接的进程注入,也就是不要dll的注入.因为dll注入据说容易触发杀软,但是弄了两天没弄出来.代码明明不报错,目标进 ...

  3. 万字博文让我们携手一起走进bs4的世界【python Beautifulsoup】bs4入门 find()与find_all()

    目录 Beautiful Soup BeautifulSoup类的基本元素 1.Tag的name 2.Tag的attrs(属性) 3.Tag的NavigableString 二.遍历文档树 下行遍历 ...

  4. 牛客周赛 Round 31(A~F)

    目录 A B C D E F A #include <bits/stdc++.h> #define int long long #define rep(i,a,b) for(int i = ...

  5. 闭关修炼180天 -- 手写SpringMVC框架(迷你版)

    SpringMvc知识须知 MVC设计模式 Model(模型):模型包含业务模型和数据模型,数据模型⽤于封装数据,业务模型⽤于处理业 务. View(视图): 通常指的就是我们的 jsp 或者 htm ...

  6. XAF新手入门 - XAF设计模式探讨

    前言 刚接触XAF的小伙伴可能会有一个疑惑,XAF中有Model(BusinessObject).View.Controller,感觉明显是一个MVC的设计模式,但当你用MVC的设计模式与其对应时,又 ...

  7. SpringBoot 学习记录 2021.05.13 Started

    环境搭建 Spring Boot 2.x Java JDK 需要安装 JDK java8 也就是 1.8, 用 jdk-8u271-windows-x64.exe 网上有很多安装java8的教程,很简 ...

  8. 已安装docker-compose,安装harbor时还是提示docker-compose未安装或者Permission denied的解决方案

    安装Harbor时,下载安装了docker-compose并赋予权限 sudo curl -L "https://github.com/docker/compose/releases/dow ...

  9. vue通用的增删改查按钮组件

    代码复用:这个组件可以在多个页面或组件中使用,避免了重复编写相同的按钮代码. 灵活性:通过showButtons属性,可以根据需要显示不同的按钮.默认情况下,它会显示添加.修改和删除按钮,但你也可以根 ...

  10. 数字政府!3DCAT实时云渲染助推上海湾区数字孪生平台

    数字孪生,是一种利用物理模型.传感器数据.运行历史等信息,在虚拟空间中构建实体对象或系统的精确映射,从而实现对其全生命周期的仿真.优化和管理的技术.数字孪生可以应用于各个领域,如工业制造.智慧城市.医 ...