Rust 生命周期浅谈

news2024/11/18 7:38:14

1. 简述

image-20240504202148065

Rust 中的每一个引用都有其 生命周期lifetime),也就是引用保持有效的作用域。大部分时候生命周期是隐含并可以推断的,正如大部分时候类型也是可以推断的一样。类似于当因为有多种可能类型的时候必须注明类型,也会出现引用的生命周期以一些不同方式相关联的情况,所以 Rust 需要我们使用泛型生命周期参数来注明他们的关系,这样就能确保运行时实际使用的引用绝对是有效的。

生命周期的概念从某种程度上说不同于其他语言中类似的工具,毫无疑问这是 Rust 最与众不同的功能。


2. 秒懂生命周期

生命周期就是一个用来避免出现悬垂引用的手段,本质上就是约束和说明变量作用域的作用关系,更好的避免哪些已经失效的数据再次被引用从而导致的一些列问题。

什么是非法引用呢?看下面这个例子:

fn main() {
    let r;
    {
        let x = 5;
        r = &x;
    }
        println!("r: {}", r);
    }
}
  • 实例代码中,我们在代码块的外部定义一个变量r,并在后续的代码块中定义一个变量x且赋值为5之后将变量x的引赋给前面r,到这里其实没什么问题。继续往下,在代码块之后将r的值打印输出,此时是无法通过编译的因为这已经出现了非法引用的问题,也就是所谓 悬垂引用
  • 这是因为x变量在执行赋值之后,截至代码#6行开始,它的作用域就结束了,也就是说,x变量的生命周期到此为止,但由于后续还存在打印r的操作,而此时由于x的结束,r所指向的数据就是一个不存在的东西,那不得报错啊。那能编译通过的话就属于玄学了。

变量 x 并没有 “存在的足够久”。其原因是 x 在到达第 7 行内部作用域结束时就离开了作用域。不过 r 在外部作用域仍是有效的;作用域越大我们就说它 “存在的越久”。

那么Rust编译器是如何直判断这段代码不能通过编译的呢?其实很简单,看的就是哪个变量的作用域存在时间更长。当然,官方将这种方式起名叫做 借用检查器

他的作用就是通过比较作用域来确保借用的合法性,避免悬垂。

image-20240504183054170

上图还是之前的示例,我使用不同的颜色以及生命周期标记来指出了变量xr的作用域,或者说生命周期时长。

  • 'a'也就是红色部分表示r的生命周期;
  • 'b'也就是亮绿色的部分表示x的生命周期;

这样就可以直观的感受到内部的 'b 块要比外部的生命周期 'a 小得多。Rust的借用检查器在编译时就会发现r引用了一个生命周期小于自己的变量x,被引用的对象比它的引用者存在的时间更

假如r在后续还需要带着x一起干一番大事业。但是发现x在这之前就西天取经去了,r也只能放弃了这个想法,人生到此结束。

换句话说,在借用关系中,被借用的对象生命周期必须大于等于借用者的生命周期,否则会出现借用者借用之后被借用的对象挂了,那借用者借了个寂寞,Rust直接拒绝编译。

所以,依据上面的原理,将代码作适当的调整之后就可以正常编译了,像下面这样。此时被借用的x的生命周期为'b且大于借用者r'a,不会出现非法借用的问题。

image-20240504184449312


3. 函数中泛型生命周期

故事还得从一个简单的方法讲起。

fn main() {
    let string1 = String::from("abcd");
    let string2 = "xyz";

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

从上面的内容不难猜测,函数longest()的作用是返回两个切片中较长的一个,功能就这么简单!

参考下面的函数实现,这种写法能逃过编译器的考验成功通过编译吗?

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

乍一看没问题啊,不就是传入两个字符串引用比较长短返回吗,为了保留实参的所有权还特地将函数参数使用了引用方式传递呢。写的挺板正的啊,语法简洁,逻辑清晰。但还是禁不住编译器的百般拷打,终于还是露出了狐狸尾巴。

image-20240504185825600

函数尝试返回 xy 的引用,但是这两个参数的生命周期并没有明确定义。在函数返回时,编译器无法确定返回的引用是否仍然有效。这和之前例子不太一样的地方就是我们没办法直观(抽象一点也可以啊)的看出来x,y的作用域,没办法确定生命周期时长,基于这个原理,Rust的借用检查器也做不到这一点。

为了解决这个问题,就需要使用泛型生命周期参数来明确指定返回引用的生命周期与输入参数的生命周期之间的关系。

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

此时代码正常执行:

image-20240504190734366

在修复后的代码中,我们使用了泛型生命周期参数 'a,这样可以确保返回的引用与输入参数的生命周期相匹配。这样编译器就能够正确推断返回引用的生命周期,避免悬垂引用或生命周期不匹配的问题。

  • 生命周期标注有着一个不太常见的语法:生命周期参数名称必须以撇号(')开头,其名称通常全是小写,类似于泛型其名称非常短。'a 是大多数人默认使用的名称。

  • 生命周期标注描述了多个引用生命周期相互的关系,而不影响其生命周期。

  • 生命周期参数标注位于引用的 & 之后,并有一个空格来将引用类型与生命周期标注分隔开。

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

看到这儿,你大概还是一知半解、一头雾水、一脸懵逼、一愣一愣。不着急,等我去画个图先,人的脑子总是惯性的偏向于理解图像信息而不是文字,尽管我文采飞扬,满屏生花!


3.1 再论泛型生命周期

通过上面泛型生命周期的简单使用大概可以获取到下面这些信息:

  • 此时通过函数签名可以明确某些生命周期'a,在函数获取到的两个参数中他们的生命周期都是和'a保持一致,对于返回值也是一个道理,也就是说,此时不论是两个参数x,y还是返回值都保持了生命周期的大小同步。
  • 怎么理解这个 同步的含义是重点,这就又和上面所学的东西关联上了,所谓的同步,就是这个生命周期标识'a会保证参数和返回值将会是三者中生命周期的较小者,可以理解为三者的交集,这也是我们需要告知rust需要保证的某种约束条件。
  • 在函数执行时,当具体的引用被传入到该函数中,'a标记的生命周期就是两个引用参数x,y的较小者(为什么不是较大者,请回去再看一遍上一个目录的内容)。
  • 保证 了x,y的约束条件之后,最终函数在返回值时,还需要再次保证此时返回值的生命周期和之前两个引用参数的生命周期的较小者。

image-20240504193644091

如上图所示。

  • 我们假设两个参数的生命周期为其较小的一方(假设为z),那么z = min(x,y);
  • w表示返回值的生命周期,那么最终返回的生命周期为min(z,w)
  • 他们之间类似于数学概念上的交集的定义,只有保证了全部生命周期中的重叠部分一致,才能保证整个函数生命周期的有效性,但凡取一个较大或者较小的值,都可能会导致非法引用问题的出现。

需要注意的是,生命周期标识仅仅作为一种标识,它本身没有更多的实际意义,也不会直接影响某个函数的功能,仅作为一种约束关系的表示而已。

这些标注出现在函数签名中,而不存在于函数体中的任何代码中。这是因为 Rust 能够分析函数中代码而不需要任何协助,不过当函数引用或被函数之外的代码引用时,让 Rust 自身分析出参数或返回值的生命周期几乎是不可能的。这些生命周期在每次函数被调用时都可能不同。这也就是为什么我们需要手动标记生命周期的原因。


理论部分巴拉完了,下面通过两个具体的例子,来直观感受下如何通过传递拥有不同具体生命周期的引用来限制 longest 函数的使用。

函数还是之前的函数,请注意观察main方法中的内容:

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);
    }
}

输出: The longest string is long string is long

这个例子中,string1的作用域显然大于string2,所以它直到整个外部作用域结束都是有效的,string2则只在{}代码块中有效,作用域较小。

result这是引用了哪些直到内部作用域结束时也还有效的值,这就相当于在string1string2中取了交集部分,二者的较小值,此时借用检查器正常检查通过,所以会看到那段输出。

没有比对就没有对比,看看下面这个例子:

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 result;
    {
        let string2 = String::from("xyz");
        result = longest(string1.as_str(), string2.as_str());
    }
    println!("The longest string is {}", result);
}

这个例子中:

  • string1直到外部作用域结束都是有效的

  • string2的作用域只在内部代码块中有效,显然在作用域范围上满足string2<string1

  • 与上一个例子比较,这里将result的声明移到了代码块之外,也即是内部作用域之外,但是它和string2的赋值操作还是留在代码块中

  • 并且打印result的代码也移到了代码块之外

通过上面的分析,这段代码显然是无法通过编译器拷打的,所以你才会看到下面的异常提示:

image-20240504195414309

  • 从人的角度读上述代码,我们可能会觉得这个代码是正确的。 string1 更长,因此 result 会包含指向 string1 的引用。因为 string1 尚未离开作用域,对于 println! 来说 string1 的引用仍然是有效的。然而,我们通过生命周期参数告诉 Rust 的是: longest 函数返回的引用的生命周期应该与传入参数的生命周期中较短那个保持一致。
  • 基于上面 保持一致 这一点,此时就应该取string2作为最终的生命周期,因为它显然比string1短,但由于此时string2在离开代码块之后就已经失效了,导致在 println! 中尝试使用 result 时,string2 已经被丢弃,从而产生了悬垂引用。是无法通过借用检查器的检查的,此时编译器收到了检查器的眼神之后,二话不说上来就是一大嘴巴子,并甩出了一句:“拒绝编译!!”

4. 参考&引用

  • 《Rust权威指南》

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1642638.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

MaxKB宝塔Docker安装并配置域名访问

准备 Linux系统 bt面板 默认环境LNMP随便装 服务器环境配置最好是4G&#xff0c; 占用硬盘存储大概1G 对于一些海外AI产品的对接需要使用香港或者海外的服务器 安装 在宝塔面板中打开SSH或者你本地使用SSH工具去链接服务器 运行docker命令 前提是放开服务器的8080端口 doc…

【深度学习基础(3)】初识神经网络之深度学习hello world

文章目录 一. 训练Keras中的MNIST数据集二. 工作流程1. 构建神经网络2. 准备图像数据3. 训练模型4. 利用模型进行预测5. (新数据上)评估模型精度 本节将首先给出一个神经网络示例&#xff0c;引出如下概念。了解完本节后&#xff0c;可以对神经网络在代码上的实现有一个整体的了…

智慧文旅展现文化新风貌,科技助力旅行品质升级:借助智慧技术,文旅产业焕发新生机,为旅行者带来更高品质的文化体验之旅

一、引言 在数字化、智能化的浪潮下&#xff0c;文旅产业正迎来前所未有的发展机遇。智慧文旅作为文旅产业与信息技术深度融合的产物&#xff0c;不仅为旅行者带来了全新的文化体验&#xff0c;也为文旅产业注入了新的活力。本文旨在探讨智慧文旅如何借助智慧技术展现文化新风…

R语言中,查看经安装的包,查看已经加载的包,查看特定包是否已经安装,安装包,更新包,卸载包

创建于&#xff1a;2024.5.4 R语言中&#xff0c;查看经安装的包&#xff0c;查看已经加载的包&#xff0c;查看特定包是否已经安装&#xff0c;安装包&#xff0c;更新包&#xff0c;卸载包 文章目录 1. 查看经安装的包2. 查看已经加载的包3. 查看特定包是否已经安装4. 安装包…

将要上市的自动驾驶新书《自动驾驶系统开发》中摘录各章片段 1

以下摘录一些章节片段&#xff1a; 1. 概论 自动驾驶系统的认知中有一些模糊的地方&#xff0c;比如自动驾驶系统如何定义的问题&#xff0c;自动驾驶的研发为什么会有那么多的子模块&#xff0c;怎么才算自动驾驶落地等等。本章想先给读者一个概括介绍&#xff0c;了解自动驾…

18 内核开发-内核重点数据结构学习

课程简介&#xff1a; Linux内核开发入门是一门旨在帮助学习者从最基本的知识开始学习Linux内核开发的入门课程。该课程旨在为对Linux内核开发感兴趣的初学者提供一个扎实的基础&#xff0c;让他们能够理解和参与到Linux内核的开发过程中。 课程特点&#xff1a; 1. 入门级别&…

Java高阶私房菜:JVM性能优化案例及讲解

目录 核心思想 优化思考方向 压测环境准备 堆大小配置调优 调优前 调优后 分析结论 垃圾收集器配置调优 调优前 调优后 分析结论 JVM性能优化是一项复杂且耗时的工作&#xff0c;该环节没办法一蹴而就&#xff0c;它需要耐心雕琢&#xff0c;逐步优化至理想状态。“…

【Gateway远程开发】0.5GB of free space is necessary to run the IDE.

【Gateway远程开发】0.5GB of free space is necessary to run the IDE. 报错 0.5GB of free space is necessary to run the IDE. Make sure that there’s enough space in following paths: /root/.cache/JetBrains /root/.config/JetBrains 原因 下面两个路径的空间不…

WPF之绑定验证(错误模板使用)

1&#xff0c;前言&#xff1a; 默认情况下&#xff0c;WPF XAML 中使用的绑定并未开启绑定验证&#xff0c;这样导致用户在UI上对绑定的属性进行赋值时即使因不符合规范内部已抛出异常&#xff08;此情况仅限WPF中的数据绑定操作&#xff09;&#xff0c;也被程序默认忽略&…

Linux设置脚本任意位置执行

记得备份 &#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01; 修改文件之后记得用 source 文件名 刷新 注意&#xff1a;刷新文件之后在当前窗口…

02.zabbix配置web界面

zabbix配置web界面 访问搭建好的地址&#xff1a; http://192.168.111.66/zabbix 检查配置都是正常&#xff0c;下一步 对应的信息&#xff0c;我设置的密码是&#xff1a;123456&#xff0c;下一步即可&#xff1b; 给服务器随意设置一个名字&#xff0c;下一步 检查数据…

022、Python+fastapi,第一个Python项目走向第22步:ubuntu 24.04 docker 安装mysql8集群、redis集群(三)

这次来安装mysql8了&#xff0c;以前安装不是docker安装&#xff0c;这个我也是第一次&#xff0c;人人都有第一次嚒 前言 前面的redis安装还是花了点时间的&#xff0c;主要是网上教程&#xff0c;各有各的好&#xff0c;大家千万别取其长处&#xff0c;个人觉得这个环境影响…

一、RocketMQ基本概述与部署

RocketMQ基本概述与安装 一、概述1.MQ概述1.1 用途1.2 常见MQ产品1.3 MQ常用的协议 2.RocketMQ概述2.1 发展历程 二、相关概念1.基本概念1.1 消息&#xff08;Message&#xff09;1.2 主题&#xff08;Topic&#xff09;1.3 标签&#xff08;Tag&#xff09;1.4 队列&#xff0…

stamps做sbas-insar,时序沉降图怎么画?

&#x1f3c6;本文收录于「Bug调优」专栏&#xff0c;主要记录项目实战过程中的Bug之前因后果及提供真实有效的解决方案&#xff0c;希望能够助你一臂之力&#xff0c;帮你早日登顶实现财富自由&#x1f680;&#xff1b;同时&#xff0c;欢迎大家关注&&收藏&&…

【计算机网络】计算机网络的定义和分类

一.定义 计算机网络并没有一个精确和统一的定义&#xff0c;在计算机网络发展的不同阶段&#xff0c;人们对计算机网络给出了不同的定义&#xff0c;这些定义反映了当时计算机网络技术的发展水平。 例如计算机网络早期的一个最简单定义&#xff1a;计算机网络是一些互连的、自…

短视频素材去哪里找免费?短视频素材从哪儿下载?

在这个数字内容为王的时代&#xff0c;视频已经成为沟通信息和吸引观众的强大工具。无论是在市场营销、教育还是娱乐领域&#xff0c;高质量的视频素材都是制作引人注目内容的关键。以下列出的网站提供多样的视频素材&#xff0c;帮助您增强视觉叙述&#xff0c;并在竞争激烈的…

查找算法与排序算法

查找算法 二分查找 (要求熟练) // C// 二分查找法&#xff08;递归实现&#xff09; int binarySearch(int *nums, int target, int left, int right) // left代表左边界&#xff0c;right代表右边界 {if (left > right) return -1; // 如果左边大于右边&#xff0c;那么…

Docker部署nginx并且实现https访问

实验环境&#xff1a; 在已有的docker环境和nginx镜像的基础上进行操作 1、生成私钥 &#xff08;1&#xff09;openssl genrsa -out key.pem 2048 生成证书签名请求 (CSR) 并自签证书: &#xff08;2&#xff09;openssl req -new -x509 -key key.pem -out cert.pem -day…

Vitis HLS 学习笔记--HLS流水线基本用法

目录 1. 简介 2. 示例 2.1 对内层循环打拍 2.2 对外层循环打拍 2.3 优化数组访问后打拍 3. 总结 1. 简介 本文介绍pipeline的基本用法。pipeline是一种用于提高硬件设计性能的技术。本文介绍了pipeline在累加计算函数中的应用。通过优化内外层循环和数组访问&#xff0c…

C#中.net8WebApi加密解密

尤其在公网之中&#xff0c;数据的安全及其的重要&#xff0c;除过我们使用jwt之外&#xff0c;还可以对传送的数据进行加密&#xff0c;就算别人使用抓包工具&#xff0c;抓到数据&#xff0c;一时半会儿也解密不了数据&#xff0c;当然&#xff0c;加密也影响了效率&#xff…