当前位置: 首页 > article >正文

【Rust自学】10.7. 生命周期 Pt.3:输入输出生命周期与3规则

喜欢的话别忘了点赞、收藏加关注哦,对接下来的教程有兴趣的可以关注专栏。谢谢喵!(=・ω・=)
请添加图片描述

10.7.1. 深入理解生命周期

1.指定生命周期参数的方式依赖于函数所做的事情

以上一篇文章的代码为例子:

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

这里的函数签名之所以这么写是因为不确定返回值到底是x还是y。如果我修改代码,比如把返回值固定为x那么就没必要给y写一个显式生命周期了:

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

所以这个代码的函数签名就没有给y限制生命周期。

2.当函数返回引用时,返回类型的生命周期参数需要与其中一个生命周期匹配

如果返回的引用没有指向任何参数,返回的内容就会变成悬空引用,因为在函数内创建的值在函数结束的时候就离开了作用域,返回的引用指向的就是被释放的内存。

看个例子:

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

在这个函数里创建了一个String类型的result,然后调用result上的as_str方法返回字符串切片(&str),其实就是一个引用,然后就报错了:

error[E0515]: cannot return value referencing local variable `result`
  --> src/main.rs:13:5
   |
13 |     result.as_str()
   |     ------^^^^^^^^^
   |     |
   |     returns a value referencing data owned by the current function
   |     `result` is borrowed here

报错信息是无法返回引用本地变量result的值,因为这块返回的值是函数内部持有的数据,其实就是刚才说的原因,当内部数据离开作用域后就会被清除。

那如果我就想要把函数内部创建值作为返回值改怎么写呢?那就不返回引用,直接返回这个值:

fn longest(x: &str, y: &str) -> String {  
    let result = String::from("Something");
    result
}

这样就相当于把函数的所有权移交给调用者了,要清理这块内存就由调用者来清理。这样写也不需要显式声明声明周期了,因为返回值与参数根本没关系,而且只有引用才有生命周期问题。

通过这个例子可以看到,生命周期的语法在根本上就是用来关联函数的不同参数以及返回值之间的生命周期的。 一旦它们取得了某种联系,Rust就获得了足够的信息来支持保证内存安全的操作并且组织可能会导致悬垂指针或是其他破坏内存安全的操作。

10.7.2. 结构体中的生命周期标注

在前面的文章里,我们在结构体中只定义过自持有的类型,比如i32String。而实际上结构体的字段也可以是引用类型,如果是引用的话就需要在每个引用上添加生命周期标注。

看个例子:

struct ImportantExcerpt<'a> {
    part: &'a str,
}

fn main() {
    let novel = String::from("Call me Ishmael. Some years ago...");
    let first_sentence = novel.split('.').next().unwrap();
    let i = ImportantExcerpt {
        part: first_sentence,
    };
}

ImportantExcerpt下只有一个字段part,其类型是字符串切片,也就是一个引用类型。因为它是引用类型,所以就需要标注生命周期。

生命周期标注的方法和泛型一样,就在结构体后面加<>,在里面写生命周期泛型类型参数即可,这里写的是'apart这个引用必须要比这个结构体实例的存活时间要长。因为只要实例存在,就会一直有part这个引用,如果part先没有,那么实例肯定会出错。

main函数,里面先创建了一个String类型的novel然后通过splitnext方法来提取出这个字符串里的第一个句子(unwrap是用来解包Option类型的,在 9.2. Result枚举与可恢复的错误 Pt.1 中有过介绍)。这个句子的类型是&str,也就是一个引用。然后创建了ImportantExcerpt这一结构体的实例i,把这个引用作为part字段的值。

这样写是没有错误的,因为first_sentence这个引用的作用域是从第7行到第11行,而i的作用域是从第8行到第11行,所以说part这个字段的存活时间比实例长并且能完全覆盖i的生命周期。

10.7.3. 生命周期的省略

每个引用都有生命周期,并且需要为使用生命周期的函数或结构体指定生命周期参数。

那为什么这段代码(来自 4.5. 切片(Slice))没有生命周期也能通过编译呢:

fn main() {
	let s = String::from("Hello world");
	let word = first_word(&s);
	println!("{}", word);
}
fn first_word(s:&str) -> &str {
	let bytes = s.as_bytes();
	for (i, &item) in bytes.iter().enumerate() {
		if item == b' ' {
			return &s[..i];
		} 
	}
	&s[..]
}

这个函数在没有生命周期注释的情况下编译的原因是有历史的:在 Rust 的早期版本(1.0 之前)中,这个代码不会通过编译,因为当时要求每个引用都需要一个显式的生命周期。函数签名就得这样写:

fn first_word<'a>(s: &'a str) -> &'a str {

后来Rust团队发现在某些特定情况下Rust程序员总会一遍又一遍地写同样的生命周期标注,而且这些场景是可预测的,这些场景有一些明确的模式,于是Rust团队就将这些模式直接写入了编译器代码,使得借用检查器在这些情况下可以自动地推导生命周期,而无需程序员显式标注。

了解这段历史的意义在于未来可能会有更多确定性模式可能会出现并被添加到编译器中。将来,可能需要更少的生命周期注释(谢天谢地)。

刚才说的这些在Rust引用分析中所编入的模式称为生命周期省略规则。这些规则无需程序员来遵守,它们是一些特殊情况,由编译器来考虑。如果你的代码符合这些情况,那就无需显式标注生命周期。

但是生命周期省略规则不会提供完整的推断,如果在应用了这个规则以后,引用的生命周期仍然模糊不清,那么仍然会引发编译错误。解决办法就是手动添加生命周期,表明引用间的相互关系。

10.7.4. 输入、输出生命周期

如果生命周期出现在函数/方法的参数中,那么这类生命周期就叫做输入生命周期

如果它出现在函数/方法的返回值中,那么就叫做输出生命周期

10.7.5. 生命周期省略的三个规则

编译器使用3个规则在没有显式标注生命周期的情况下来确定引用的生命周期

  • 规则1用于输入生命周期
  • 规则2、3用于输出生周期
  • 如果编译器在应用完3个规则后仍然有无法确定生命周期的引用,就会报错
  • 这3个规则不但适用于函数或是方法的定义,也适用于impl

规则1: 每个引用类型的参数都有自己的生命周期。 单参数的函数就有1个生命周期,双参数的函数就有两个,以此类推。

规则2: 如果只有1个输入生命周期参数,那么该生命周期被赋给所有的输出生命周期参数。 就是单参数的生命周期只有1个,这个生命周期就是这个函数所有可能返回值的生命周期。

规则3: 如果有多个输入生命周期参数,但其中一个是&self&mut self(也就是说是这个函数是方法),那么self的生命周期会被赋给所有输出的生命周期参数。

1. 成功例

规则讲完,看看例子:

fn first_word(s:&str) -> &str {
	//...
}

把自己带入一下编译器,想想对于这个函数签名如何根据3条规则来找到省略的生命周期。

首先应用第一条规则——每个引用类型的参数都有自己的生命周期。这里只有一个参数,所以就只有一个生命周期。所以到这一步编译器推断出了:

fn first_word<'a>(s:&'a str) -> &str {
	//...
}

由于只有一个输入生命周期,所以第2条规则在这里也适用——如果只有1个输入生命周期参数,那么该生命周期被赋给所有的输出生命周期参数。 所以输入生命周期就被赋予给了输出生命周期。到这一步编译器推断出了:

fn first_word<'a>(s:&'a str) -> &'a str {
	//...
}

由于只有一个输入生命周期,且这个函数不是方法,所以第3条不适用

而现在函数中所有的引用都有了生命周期,因此编译器就可以继续分析代码,而无需程序员手动标注这个函数签名里的生命周期。

2. 失败例

来看第二个例子:

fn longest(x:&str, y:&str) -> &str {
	//...
}

这个函数签名有两个引用输入,返回类型也是引用。尝试用这3条规则:

首先应用第一条规则——每个引用类型的参数都有自己的生命周期。这里有两个参数,就有两个生命周期:

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

由于有两个引用参数,所以规则2不适用。

由于这个函数不是方法,所以规则3不适用。

应用完这3条规则后发现返回值的生命周期仍然无法确定,所以编译器就会报错。也就是说你必须显式声明生命周期。


http://www.kler.cn/a/471923.html

相关文章:

  • Git撤销指定commit并更新远端仓库
  • 关于FPGA中添加FIR IP核(采用了GOWIN EDA)
  • nginx http反向代理
  • sunrays-framework(太阳射线框架搭建)
  • C++ constexpr(八股总结)
  • 【C++数据结构——查找】二叉排序树(头歌实践教学平台习题)【合集】
  • java ShaUtils sha1如何生成签名?
  • uniapp下的手势事件
  • Vue 3 详解
  • 锂电池剩余寿命预测 | 基于BiLSTM-Attention的锂电池剩余寿命预测,附锂电池最新文章汇集
  • 2、zookeeper和kafka
  • List ---- 模拟实现LIST功能的发现
  • 23.行号没有了怎么办 滚动条没有了怎么办 C#例子
  • IP Anycast 与 CDN
  • c/c++ 里的进程间通信 , 管道 pipe 编程举例
  • 接口项目架构流程图-thinkphp6-rabbitmq
  • QT学习十九天 QT核心机制
  • WebRtc02: WebRtc架构、目录结构、运行机制
  • Lianwei 安全周报|2024.1.7
  • 【Java】JVM内存相关笔记
  • 代理IP授权机制:保障安全与效率的双重考量
  • matlab专栏-常见问题处理
  • 青少年编程与数学 02-006 前端开发框架VUE 12课题、表单绑定
  • C#高级:递归4-根据一颗树递归生成数据列表
  • 通过 ulimit 和 sysctl 调整Linux系统性能
  • ThinkPHP 8高效构建Web应用-获取请求对象