Rust 所有权、引用、生命周期
变量、值、所有权
变量、值
- 变量:用来代表值进行操作、没有对应内存空间的字符串, 如:a,b
- 值:在内存中有对应的空间,如:5,”asdf”
变量默认不可变
var变量绑定的值1不能更改,对应的内存数据不能改变
不允许赋值操作,但是变量的声明和绑定可以分开,即使 声明不可变变量,也可以之后绑定值,注意区分
1
2
3
4
5
6
7
8fn ret_int() -> i32{ 5 }
// 以下代码可编译,且正确
let num = &mut ret_int();
*num += 1;
// 以下不可
let num;
num = &mut ret_int();
*num += 1;todo
不允许获取可变引用、所有权转移给可变变量(对函数 即限制参数类型,类似于默认const)
若其中包含(或就是)引用,引用值是可以更改的
1
2let ref = &mut ori;
*ref += 1;但是变量var可以重新绑定为其他的值
虽然值1不能更改,但是var变量可以绑定其他值
1
2let var = 3;
let var = 2;此时虽然值1虽然无法被访问、使用,但是离开作用域 之前不会被丢弃,只是被“隐藏”
所有权规则
每个值(内存)都有一个称为所有者(owner)的变量
值(内存)有且只有一个所有者
- 如果多个变量拥有某值(内存)所有权,有可能会多次释放 同一内存,造成内存二次污染
- rust中只有一个变量拥有所有权避免内存污染问题
所有者(变量)离开作用域,值将被丢弃(内存被回收) (rust为其调用
drop
函数)在生命周期结束时释放资源的方法在C++称为RAII (Resource Aquistion Is Initialization),这里的 initialization是指对资源跟踪、管理初始化,RAII就是 将对象(变量)同资源生命周期相关联,在C++中体现为 析构函数
- rust的所有权管理是编译是进行检查,没有runtime性能损失
- 相较于gc(垃圾回收)性能影响较小
- 相较于手动分配、释放内存不容易代码疏忽导致的内存问题
所有权转移
Drop
和Copy
trait
Drop
:值离开作用域时rust自动调用Copy
:赋值时调用,赋值之后原变量仍然可以继续使用
需要分配内存、本身就是某种资源形式不会实现Copy
trait,实现
Copy
trait类型有
- 存储在stack上的类型,值拷贝速度快,定长
- 整型、bool型、浮点型这样(标量)原生数据类型
- 所有元素都copy的tuple
rust不允许任何类型同时同时实现Drop
和Copy
trait
所有权转移
对于没有实现Copy
trait的类型:赋值(包括函数传参、返回值)
操作会将原变量所有权转移(move)给新变量,之后不允许使用
原变量,编译时即报错,这样就避免了同时释放同一内存造成
二次污染
有些情况下所有权不允许转移,如vec中元素
实现了Copy
trait的类型,赋值(包括函数传参)操作将按照
Copy
trait复制一个新值,将新值(包括所有权)赋给新变量,
如此原变量可以之后继续使用,没有违反rust的所有权规则,因为
实际上两个值(内存)
Reference(引用)
引用(references,&,获取引用作为函数参数称为借用)
单一所有权情况下,仅仅想使用值而不获取所有权,尤其是函数 传参,虽然可以获取所有权之后再将所有权转移,但是操作麻烦, 而且函数返回值可能有其他用途
引用特点
- 引用允许变量使用值但是不获取所有权
- 引用离开作用域时不会丢弃其指向的值,不会内存二次污染
- 分为可变引用、不可变引用,类似于变量绑定
引用规则
- 任意时间内,特定作用域、特定变量只允许
- 一个可变引用
- 任意数量不可变引用
- 引用必须总是有效的
注意:获取可变引用和引用赋值给可变变量的区别
1 | let m = 5; |
规则1
第一条规则避免以下情况导致的数据竞争
- 多个指针可以访问同一数据
- 至少有一个指针可以写入数据
- 没有有效的同步数据访问机制
这条规则在显式的赋值、声明易发现、遵守,需要注意的是
- 函数调用创建可变引用
- 自运算符创建可变引用(
+=
、*=
)
规则2
rust会在编译时检查引用是否有效,即引用是否是悬垂指针
悬垂指针:指向的内存已经被分配给其他所有者或值被丢弃, 常见于函数中返回局部变量
对于rust中的“变量(有所有权)”而言则不存此问题
- 赋值操作要么转移所有权,值不会被丢弃
- 要么实现
copy
trait,返回新值
解引用
rust中引用更像c中的指针
引用有对应的解引用(dereferance),且“多层次”引用也需要 ”多层次“解引用
教程上的引用附图也是引用“指向”原“变量”,不是“值(内存)”
自动引用和解引用
生命周期
- rust每个引用都有生命周期,即引用保持有效的作用域 (避免悬垂引用)
大部分情况下,生命周期是隐含、可推断的 (类似于类型可推断)
- 借用检查器(编译器一部分)可以分析函数代码得到引用的 生命周期,通过作用域确保借用有效
- 但是函数被调用、被函数之外的代码引用时,每次生命周期 都不一样,rust无法分析
有时也会出现引用生命周期以一些不同的方式相关联,此时需要 使用泛型生命周期参数标注关系
生命周期注解语法
- 生命周期参数必须以”’“开头(和一般泛型参数区别)
- 参数名称通常小写
- 位于引用”&“之后,”mut“(如果存在)之前
函数生命周期注解
只存在于函数签名中
1 | fn longest<'a>(x: &'a str, y: &'a str) -> &'a str{ |
生命周期注解并不改变参数、返回值的生命周期,只是在 函数签名中增加了生命周期“协议”
- 函数体中返回值不遵守“协议”,函数体编译错误
- 传参、返回值接收变量不遵守“协议”,调用处编译错误
- 生命周期注解是用于联系函数不同参数和返回值的生命 周期,一旦形成某种联系,编译器就能获取足够信息判断 引用是否有效(是否内存安全、产生悬垂指针)
生命周期注解是为了保证函数返回值引用有效,同函数 用途有关,因此以下签名也可以通过编译
1
2
3fn longest<'a>(x: &'a str, y: &str) -> &a' str{
x
}
生命周期注解省略规则
- 每个是引用的参数都有自己的输入生命周期参数
- 如果只有一个输入生命周期参数,会被赋予所有输出生命周期 参数
- 若方法存在多个输入生命周期参数,且首个参数
self
为引用(&self
、&mut self
),将其生命周期参数赋给 所有输出生命周期参数
编译器检查完以上三条规则之后,所有引用均有生命周期参数,则 无需额外生命周期注解,但是若函数体返回值不遵守“协议”,仍 无法编译通过
1 | impl<'a> stct<'a>{ |
这个例子说明生命周期注解不只是“注解”,是真的需要例子考虑 返回值的生命周期
结构体生命周期注解
在结构体成员为引用时需要增加生命周期注解
1 | struct ImportantExcerpt<'a>{ |
此类结构体在实现方法时不能省略生命周期注解,方法签名可根据 规则省略注解
1 | impl<'a> ImportantExcerpt<'a>{ |
泛型结构体(枚举)作为函数参数、返回值类型时,替换泛型参数 为引用时也需要添加生命周期注解
1 | fn new(args: &[String]) -> Result<Config, &'a str>{} |
静态生命周期('static
)
- 存活于整个程序生命期间
- 所有的
&str
(字符串字面值)都拥有'static
生命周期 - 可以用于指定引用的生命周期,但是使用之前三思,应先考虑 悬垂引用、生命周期不匹配的问题
高级生命周期
Lifetime Subtyping
生命周期子类型:确保某个生命周期长于另一个生命周期
1 | struct Context<'s>(&'s str); |
Lifetime Bounds
生命周期bounds:帮助Rust验证泛型引用不会比其引用的数据存在 更久
1 | struct Ref<'a, T: 'a> { &'a T }; |
trait对象生命周期推断
1 | trait Red {} |
以上代码能编译通过,因为生命周期和trait对象必须遵守:
- trait对象默认的生命周期为
'static
- 若有
&'a X
或&'a mut x
,则默认生命周期为'a
- 如有
T: 'a
从句,则默认生命周期为'a
- 若有多个类似
T: 'a
从句,则需明确指定trait对象生命周期,Box<Red + 'a>
或Box<Red + 'static>
todo
正如其他bound,任何Red
trait的实现内部包含引用,必须拥有和
trait对象bound中所指定的相同的生命周期
Rust 所有权、引用、生命周期