首页 > 其他分享 >Rust生命周期的理解

Rust生命周期的理解

时间:2022-12-30 15:35:35浏览次数:24  
标签:生命周期 String i32 Rust 理解 引用 fn

前言

这篇文章的目的是让读者最快最直观的了解什么是生命周期,以及为什么有生命周期,为了达到这个目的——即降低复杂性,本篇文章的用词可能不够严谨,见谅。

引用和所有者

所有者

为了保证一个值会在它作用域结束时被销毁,Rust引入了所有权机制。

fn noname() {
	let a = String::from("ABCDEFG");
	b = a;
}

上面的代码会发生什么?

  • let a = String::from("ABCDEFG");将后面的String值给到变量a,现在,可以说这个a就是字符串值的所有者
  • b = a;,将a中的字符串值的所有权转移给b,现在,b是字符串值的所有者,a失效了。

所有者负责该变量的销毁,在Rust中,当所有者所在的作用域结束,其值就会被自动销毁,所以:

fn noname() {
	let a = String::from("ABCDEFG");
	b = a;
} // b的值在这里被销毁,a由于已经不拥有值,所以无需处理

但大多数时候,你都希望只是简单的访问或者修改下值,而不是拿到它的所有权,比如,你不想在方法调用时把一个变量传进去,然后它就不能用了:

fn func(string: String) { // 参数值的所有权被转移给变量string
    // do something
} // string脱离作用域,值被销毁

fn main() {
	let a = String::from("ABCDEFG");
    func(a); // a对其值的所有权转移到函数中,a不再拥有任何值
    println!("{}", a); // 访问没有值的变量,无法通过编译
}

这个时候,你需要用到引用。

引用

fn func(string: &String) { // string是一个String类型的引用,并不拥有其值
    // do something
} // string引用被销毁,不影响值

fn main() {
	let a = String::from("ABCDEFG");
    func(&a); // 产生a的一个引用
    println!("{}", a); // 正常编译!
}

我们在这篇文章里不涉及引用到底是什么样的一个东西,它和指针有什么区别,引用在堆上还是栈上等问题,这些问题,你可以去看《Rust In Action》,你可以暂时把它想成一个指向原值的指针,但这个指针不同于C的指针,引用有额外的安全机制,即Rust会保证引用永远不会指向失效的值

引用的生命周期

引用有额外的安全机制,即Rust会保证引用永远不会指向失效的值

问题就出在这句话上,有了这句话,才有了生命周期这个小碧7。还是刚刚的例子:

fn func(string: &String) {
    // Rust保证引用不会指向失效的值,所以在该方法中你可以放心大胆的访问string而不会出现任何内存错误
    // 也就是说,string的原值在方法调用期间必须一直是有效的
}

为了满足上面的保证,Rust必须确保,传入func的引用,其原值的存活时间大于等于func的作用域,只有这样,引用才不会指向失效的值(实际上作为参考的并不是func的作用域,不过在这个例子上先这样理解):

img

我们可以把这个存活时间理解为生命周期。

当引用被创建,一个引用的生命周期开始,当引用结束最后一次使用,该引用的生命周期结束。更复杂一点的话,引用的生命周期可以是分离开的多个区域,这通常在有流程控制结构时发生,在这篇文章里我们不会触碰这些复杂的结构,可以在《Rust for Rustaceans》一书的第一章中找到。

引用的生命周期,必须小于等于其值的生命周期

我来举一个反例:

fn invaild_ret_ref() -> &String {
	&String::from("Hello")
}

这个函数返回一个String的引用,它在函数中使用String::from创建了一个String值,然后试图将它的引用返回,可是在invaild_ret_ref函数结束后,这个String值就已经被销毁了,该引用不可能能够指向有效的数据,Rust编译器会拒绝这个代码。

我很喜欢张汉东老师对《Rust for Rustaceans》的翻译样章中把生命周期(lifetime)翻译成生存期,生存期确实比生命周期更加生动,但为了和已有的术语对应,这里我还是使用生命周期。

生命周期标记

正如我们看到的,Rust的引用代表对值的一次借用,它们有着种种限制,所以,在函数中、在结构体中、在方法等等位置上使用引用时,你都要给Rust编译器一些关于引用的提示,这种提示,就是生命周期标记

考虑下面的例子:

fn longer_one(a: &String, b: &String) -> &String {
	if a.len() > b.len() {
		a
	} else {
		b
	}
}

我不是要故意把例子写的复杂,因为对于简单的情况,Rust编译器已经足够聪明到让你能够省略生命周期标识符,稍后我们会谈到。

这个函数接收两个String引用,返回其中较长的那个引用。首先先说结论,它无法通过编译。

为什么呢?考虑下面的调用:

fn main() {
	let str_1 = String::from("Hello");
	let longer: &String;

	{
		let str_2 = String::from("A");
		longer = longer_one(&str_1, &str_2);
	} // str_2的值被销毁

	println!("{}", longer);
} // str_1的值被销毁

这个调用有什么问题呢?str_1的值Hello,它的生命周期是整个main函数,直到main结束,它才会被销毁,而str_2的值A,它在它所在的代码块结束后就被销毁。所以,它们的引用有效的生命周期范围也是不同的,str_2的引用的生命周期就是中间的那个只有两行代码的代码块。

引用类型变量longer接收longer_one函数的返回结果,该函数可能返回str_1的引用和str_2的引用其中之一,如果恰好str_1比较长,那么longer变量就是str_1的引用,我们可以在后面的println语句中访问它,因为它仍处在自己的生命周期中,而如果是str_2较长,我们稍后如果访问它就是在访问一个无效的内存值,这是很危险的。

也就是说,在这种情况下,Rust编译器无法确定哪个引用是被返回的,如果Rust放任这种情况,让这种模棱两可的代码通过编译,那么你的程序偶尔会正常运行,偶尔会出现内存错误。

如果你已经感到晕乎乎的了,不妨休息一下。

如果只返回a呢?

上面的代码从本质上就有问题,无论你使用还是不用生命周期标记,都无法解决这个问题,生命周期标记也不是为了处理这个问题而存在的。不过,我们考虑下面的代码:

fn longer_one(a: &String, b: &String) -> &String {
	a
}

现在,该方法被修改了,它只会返回第一个引用,也就是a,这次,按理说上面的调用不会出现问题了,因为str_2的引用将永远不可能被赋值给longer,后面的访问也没有风险。

但是Rust编译器无法识别这种情况,该函数还是无法通过编译,而生命周期标记,本质目的就是为了在你能够确保你的代码正常的情况下,提示下Rust编译器,我不会出错,放我过去吧

给编译器关于引用生命周期的提示

Rust的生命周期标识是通过类似泛型的语法来定义的,不得不说,看起来很丑,且没有道理:

fn longer_one<'a>(a: &'a String, b: &String) -> &'a String {
	a
}

在这里,我们定义了一个生命周期标记'a,生命周期必须以引号开头。我们告诉编译器,参数a这个引用的生命周期是'a,并且我返回值的生命周期和它的生命周期相同,也是'a

此时,你再进行编译,代码已经可以通过编译了,外面的代码清楚的知道在第二个参数位置上的str_2是不会被返回并赋值给longer的,所以,编译器可以放过我们的代码。

关于为什么使用泛型语法,下面是我自己的猜测:

考虑这样的函数签名:fn test<T>(a: T) -> T,泛型允许我们传入任何类型,T可以是任意类型,但是,最主要的,泛型约束了该函数的参数a和返回值的类型必须是相同的。换句话说,当a的类型确定了,返回值类型就确定了。

回想刚刚的生命周期泛型标记,'a可以是任意生命周期(实际上仍有一些限制,我们稍后会说),但是,泛型生命周期约束了该函数的返回值必须和a的生命周期一致,当a的生命周期确定了,返回值的生命周期就确定了。

你可以先按上面的理解,但在生命周期中,事情会稍微有些不一样,我们马上会看到。

和泛型差在哪?

我们可以使用比要求更大的生命周期

为了方便演示,且不引入&str类型(因为对于初学者来说,String可能会和&str混淆),我们把例子换成i32类型,还是之前一样的代码:

fn first<'a>(a: &'a i32, b: &i32) -> &'a i32 {
	a
}

我应该不用再解释该代码的意思以及为什么它能够通过编译,而去掉了生命周期标记'a就不能了,你可以自己试着讲下。

现在,我们看一个神奇的东西:

static GLOBAL: &i32 = &6i32;
fn first<'a>(a: &'a i32, b: &i32) -> &'a i32 {
	GLOBAL
}

该代码的返回值并不和参数'a具有相同生命周期,但是该代码也能通过编译。为什么呢?我们上面的解释站不住脚了吗?

确实,站不住脚了!!!!上面,为了让你理解为什么Rust使用泛型语法来标记生命周期描述符,我给你讲了一些并非事实的东西,虽然无伤大雅。

在泛型中:

fn test<T>(a: T) -> T {
    // dosomething
    a
}

当参数a传入,它的类型被确定,T就等于这个类型了,你不可以再使用一个其它类型来代表它。

而在泛型生命周期上:

static GLOBAL: &i32 = &6i32;
fn first<'a>(a: &'a i32, b: &i32) -> &'a i32 {
	GLOBAL
}

泛型生命周期标记'a不是这样,当你传入a,你可以在需要a的生命周期时使用大于等于参数a的生命周期,而并非必须和它一致。

回到上面的代码,GLOBAL是一个static变量,它的值是一个引用类型,static变量的生命周期是'static,它是贯穿整个程序始终的。也许后面我们会详细介绍它,我们可以在需要

如果考虑类型兼容性,如继承和实现关系,其实泛型也有这种特性,当然Rust里没有。

插曲:防止你理解歪了因为我就理解歪了

虽然你可以在需要一个生命周期时,使用另一个更大的生命周期去代表它,但是,从上面代码的角度说,外界并不知道你返回的引用具有'static生命周期,它依然会认为,你返回的是第一个参数的生命周期。

static GLOBAL: &i32 = &6i32;
fn first<'a>(a: &'a i32, b: &i32) -> &'a i32 {
	GLOBAL
}

你在这样使用它时,它还会拒绝你的代码:

let ret: &i32;
{
    let a: i32 = 12;
    let b: i32 = 14; 
    ret = first(&a, &b);  // 已经被借用的a的值的存活时间不够
} // a在这里被销毁,但它仍被借用
println!("{}", ret); // 它会在这里被借用

代表更小的一个生命周期

下面的代码的生命周期如何解释?:

fn longer_one<'a>(a: &'a String, b: &'a String) -> &'a String {
	if a.len() > b.len() {
		a
	} else {
		b
	}
}

参数ab都具有生命周期'a,并且返回值也具有生命周期'a,如果参数a和参数b这两个引用的生命周期一致,显然就无需解释了,'a就是它们两个的生命周期,可万一它们的生命周期不一致呢?

假如a的更大,b的更小,那么很自然的,返回值代表更小的那个生命周期,外部会将返回值的生命周期当作ab中更小的那个来用。

生命周期标记作用于多个引用时,其代表最小的那个引用的生命周期

总结

  • 生命周期是一个引用的生存期,在代码编译时,Rust会确保引用的生命周期不会超过其值的有效范围
  • 在Rust编译器感到迷惑的时候,你得给它一些有关生命周期的提示,这种提示叫做生命周期标注
  • 生命周期标注使用泛型语法声明,比如<'a>
  • 在引用上使用生命周期标注的语法是&'a Type
  • 生命周期和泛型不一样的地方在于,在需要一个生命周期的位置,你可以使用一个更大的生命周期代表。在泛型生命周期应用于多个引用上时,其取最小的那个引用的生命周期。

如果你已经感到晕乎乎的了,不妨休息一下。

在结构体中使用生命周期

未完...

标签:生命周期,String,i32,Rust,理解,引用,fn
From: https://www.cnblogs.com/lilpig/p/17014977.html

相关文章

  • Qt总结_对象模型_组件parent的理解
    标准C++对象模型在运行时效率方面卓有成效,但是在某些特定问题域下的静态特性就显得捉襟见肘。GUI界面需要同时具有运行时的效率以及更高级别的灵活性。为了解决这一问题,Q......
  • 转载:调用规范stdcall、cdecl、fastcall、thiscall 、naked call的汇编理解 (https://ww
    当高级语言函数被编译成机器码时,有一个问题就必须解决:因为CPU没有办法知道一个函数调用需要多少个、什么样的参数。即计算机不知道怎么给这个函数传递参数,传递参数的工作......
  • 智能制造 | AIRIOT智慧工厂管理解决方案
    工厂生产运转中,设备数量多,环境复杂、企业往往需要承担很高的维修、保养、备件和人力成本。传统的工厂改革遇到了诸多前所未有的挑战:1、管理系统较多,数据隔离,系统集成困难重......
  • 理解iOS端的WebView同层组件
    理解iOS端的WebView同层组件一起始同层渲染是利用原生技术来优化Web渲染一种技术,很多人了解它是起于微信开放社区发布的一篇关于小程序渲染原理剖析的文章。我将链接附上......
  • Rust 语言新人入门指南
    首先,学习Rust不能急躁。如果你抱着之前1天上手Python,2天入门Go的经验和优越感来学习Rust的话,你可能会遭遇严重的失败感。如果你来自Haskell/Ocaml等函数式语......
  • Dubbo 3 之 Triple 流控反压原理解析
    作者:顾欣Triple是Dubbo3提出的基于HTTP2的开放协议,旨在解决Dubbo2私有协议带来的互通性问题。Triple基于HTTP/2定制自己的流控,支持通过特定的异常通知客户......
  • Vue生命周期
    官网解释一、Vue的生命周期Vue实例有⼀个完整的⽣命周期,也就是从开始创建、初始化数据、编译模版、挂载Dom->渲染、更新->渲染、卸载等⼀系列过程,称这是Vue的⽣......
  • Dubbo 3 之 Triple 流控反压原理解析
    作者:顾欣Triple是Dubbo3提出的基于HTTP2的开放协议,旨在解决Dubbo2私有协议带来的互通性问题。Triple基于HTTP/2定制自己的流控,支持通过特定的异常通知客户......
  • 关于正向传播、反向传播、梯度爆炸、梯度消失的理解
    假设有这样一个神经网络,包含一个输入层I,一个隐藏层H,一个输出层O  其中,输入层包含两个神经元,分别为i1和i2;隐藏层有两个神经元,分别为h1和h2;输出层有两个神经元,分别为o1......
  • [Spring] Spring 中bean的生命周期
    在平时的工作中,我们的很多项目都是利用Spring进行搭建的。最近有空,基于源码好好分析一下,Bean在Spring中的生命周期这里我们先写一个简单的小例子<?xmlversion="1.0"e......