Rust 所有权、引用、生命周期

变量、值、所有权

变量、值

  • 变量:用来代表值进行操作、没有对应内存空间的字符串, 如:a,b
  • 值:在内存中有对应的空间,如:5,”asdf”

变量默认不可变

  • var变量绑定的值1不能更改,对应的内存数据不能改变

    • 不允许赋值操作,但是变量的声明和绑定可以分开,即使 声明不可变变量,也可以之后绑定值,注意区分

      1
      2
      3
      4
      5
      6
      7
      8
      fn ret_int() -> i32{ 5 }
      // 以下代码可编译,且正确
      let num = &mut ret_int();
      *num += 1;
      // 以下不可
      let num;
      num = &mut ret_int();
      *num += 1;

      todo

    • 不允许获取可变引用、所有权转移给可变变量(对函数 即限制参数类型,类似于默认const)

    若其中包含(或就是)引用,引用值是可以更改的

    1
    2
    let ref = &mut ori;
    *ref += 1;
  • 但是变量var可以重新绑定为其他的值

    • 虽然值1不能更改,但是var变量可以绑定其他值

      1
      2
      let var = 3;
      let var = 2;
    • 此时虽然值1虽然无法被访问、使用,但是离开作用域 之前不会被丢弃,只是被“隐藏”

所有权规则

  • 每个值(内存)都有一个称为所有者(owner)的变量

  • 值(内存)有且只有一个所有者

    • 如果多个变量拥有某值(内存)所有权,有可能会多次释放 同一内存,造成内存二次污染
    • rust中只有一个变量拥有所有权避免内存污染问题
  • 所有者(变量)离开作用域,值将被丢弃(内存被回收) (rust为其调用drop函数)

    在生命周期结束时释放资源的方法在C++称为RAII (Resource Aquistion Is Initialization),这里的 initialization是指对资源跟踪、管理初始化,RAII就是 将对象(变量)同资源生命周期相关联,在C++中体现为 析构函数

  • rust的所有权管理是编译是进行检查,没有runtime性能损失
  • 相较于gc(垃圾回收)性能影响较小
  • 相较于手动分配、释放内存不容易代码疏忽导致的内存问题

所有权转移

DropCopytrait

  • Drop:值离开作用域时rust自动调用
  • Copy:赋值时调用,赋值之后原变量仍然可以继续使用

需要分配内存、本身就是某种资源形式不会实现Copy trait,实现 Copy trait类型有

  • 存储在stack上的类型,值拷贝速度快,定长
  • 整型、bool型、浮点型这样(标量)原生数据类型
  • 所有元素都copy的tuple

rust不允许任何类型同时同时实现DropCopytrait

所有权转移

对于没有实现Copytrait的类型:赋值(包括函数传参、返回值) 操作会将原变量所有权转移(move)给新变量,之后不允许使用 原变量,编译时即报错,这样就避免了同时释放同一内存造成 二次污染

有些情况下所有权不允许转移,如vec中元素

实现了Copytrait的类型,赋值(包括函数传参)操作将按照 Copytrait复制一个新值,将新值(包括所有权)赋给新变量, 如此原变量可以之后继续使用,没有违反rust的所有权规则,因为 实际上两个值(内存)

Reference(引用)

引用(references,&,获取引用作为函数参数称为借用

单一所有权情况下,仅仅想使用值而不获取所有权,尤其是函数 传参,虽然可以获取所有权之后再将所有权转移,但是操作麻烦, 而且函数返回值可能有其他用途

引用特点

  • 引用允许变量使用值但是不获取所有权
  • 引用离开作用域时不会丢弃其指向的值,不会内存二次污染
  • 分为可变引用、不可变引用,类似于变量绑定

引用规则

  • 任意时间内,特定作用域、特定变量只允许
    • 一个可变引用
    • 任意数量不可变引用
  • 引用必须总是有效的

注意:获取可变引用引用赋值给可变变量的区别

1
2
3
4
5
6
let m = 5;
let mut n = &m;
//将引用赋值给可变变量
let n = &mut m;
//获取可变引用赋值给变量,这里会报错,因为`m`是不可变
//变量,不允许通过其获取可变引用

规则1

第一条规则避免以下情况导致的数据竞争

  • 多个指针可以访问同一数据
  • 至少有一个指针可以写入数据
  • 没有有效的同步数据访问机制

这条规则在显式的赋值、声明易发现、遵守,需要注意的是

  • 函数调用创建可变引用
  • 自运算符创建可变引用(+=*=

规则2

rust会在编译时检查引用是否有效,即引用是否是悬垂指针

悬垂指针:指向的内存已经被分配给其他所有者或值被丢弃, 常见于函数中返回局部变量

对于rust中的“变量(有所有权)”而言则不存此问题

  • 赋值操作要么转移所有权,值不会被丢弃
  • 要么实现copytrait,返回新值

解引用

rust中引用更像c中的指针

  • 引用有对应的解引用(dereferance),且“多层次”引用也需要 ”多层次“解引用

  • 教程上的引用附图也是引用“指向”原“变量”,不是“值(内存)”

自动引用和解引用

  • 方法中self
  • +自己实现了解引用(不知道是否算自动解引用)
    1
    2
    impl<'a> Add<&'a i32> for i32{}
    impl Add<i32> for i32{}
  • todo

生命周期

  • rust每个引用都有生命周期,即引用保持有效的作用域 (避免悬垂引用)
  • 大部分情况下,生命周期是隐含、可推断的 (类似于类型可推断)

    • 借用检查器(编译器一部分)可以分析函数代码得到引用的 生命周期,通过作用域确保借用有效
    • 但是函数被调用、被函数之外的代码引用时,每次生命周期 都不一样,rust无法分析
  • 有时也会出现引用生命周期以一些不同的方式相关联,此时需要 使用泛型生命周期参数标注关系

生命周期注解语法

  • 生命周期参数必须以”’“开头(和一般泛型参数区别)
  • 参数名称通常小写
  • 位于引用”&“之后,”mut“(如果存在)之前

函数生命周期注解

只存在于函数签名

1
2
3
4
5
6
7
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str{
if x.len() > y.len(){
x
}else{
y
}
}
  • 生命周期注解并不改变参数、返回值的生命周期,只是在 函数签名中增加了生命周期“协议”

    • 函数体中返回值不遵守“协议”,函数体编译错误
    • 传参、返回值接收变量不遵守“协议”,调用处编译错误
    • 生命周期注解是用于联系函数不同参数和返回值的生命 周期,一旦形成某种联系,编译器就能获取足够信息判断 引用是否有效(是否内存安全、产生悬垂指针)
  • 生命周期注解是为了保证函数返回值引用有效,同函数 用途有关,因此以下签名也可以通过编译

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

生命周期注解省略规则

  • 每个是引用的参数都有自己的输入生命周期参数
  • 如果只有一个输入生命周期参数,会被赋予所有输出生命周期 参数
  • 方法存在多个输入生命周期参数,且首个参数self 为引用(&self&mut self),将其生命周期参数赋给 所有输出生命周期参数

编译器检查完以上三条规则之后,所有引用均有生命周期参数,则 无需额外生命周期注解,但是若函数体返回值不遵守“协议”,仍 无法编译通过

1
2
3
4
5
6
7
impl<'a> stct<'a>{
fn other_str(&self, &str1) -> &str{
str1
}
}
// 检查完规则之后,所有引用均有注解,但是
// 函数体中的生命周期和签名中不一致

这个例子说明生命周期注解不只是“注解”,是真的需要例子考虑 返回值的生命周期

结构体生命周期注解

在结构体成员为引用时需要增加生命周期注解

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

此类结构体在实现方法时不能省略生命周期注解,方法签名可根据 规则省略注解

1
2
3
4
impl<'a> ImportantExcerpt<'a>{
fn (&self){
}
}

泛型结构体(枚举)作为函数参数、返回值类型时,替换泛型参数 为引用时也需要添加生命周期注解

1
2
fn new(args: &[String]) -> Result<Config, &'a str>{}
fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str>{}

静态生命周期('static

  • 存活于整个程序生命期间
  • 所有的&str(字符串字面值)都拥有'static生命周期
  • 可以用于指定引用的生命周期,但是使用之前三思,应先考虑 悬垂引用、生命周期不匹配的问题

高级生命周期

Lifetime Subtyping

生命周期子类型:确保某个生命周期长于另一个生命周期

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
struct Context<'s>(&'s str);
struct Parser<'c, 's: 'c>{
//`'s: 'c`声明一个不短于`'c`的生命周期`'s`
context: &'c Context<'s>;
}
impl<'c, 's: 'c> Parser<'c, 's: 'c>{
fn parse(&self) -> Result<(), &'s str>{
//根据生命周期省略规则,若`'s`省略,则赋予`&self`的
//生命周期
//使用生命周期子类型语法,指定(要求)`&str`生命周期
//长于`&Context`
Err(&self.context.0[1..]
//这里没有考虑字符串切片的有效性,如果这个切片不是
//有效的unicode字符串(utf8字节序列),会panic
}
}
fn parse_context(context: Context) -> Result<(), &str>{
//方法获取`context`的所有权
Parser{ context: &Context }.parse()
//`&Context`的生命周期只有整个函数内
//函数体中的返回值是`context.0[1..]`,为保证返回值有效,
//其生命周期必须长于整个函数
//返回值中的`&str`类型的生命周期是`'s`,长于context`'c`
//满足返回值的生命周期长于函数的要求,能编译通过
}

Lifetime Bounds

生命周期bounds:帮助Rust验证泛型引用不会比其引用的数据存在 更久

1
2
3
4
5
struct Ref<'a, T: 'a> { &'a T };
//为`T`增加生命周期bound,指定`T`引用的生命周期不短于
//`'a`,保证结构体成员有效
struct StaticRef<T: 'static> { &'static T };
//限制`T`为只拥有`'static`生命周期的引用或没有引用的类型

trait对象生命周期推断

1
2
3
4
5
6
7
8
9
trait Red {}
struct Ball<'a> {
diameter: &'a i32,
}
impl<'a> Red for Ball<'a> {}
fn main(){
let num = 5;
let obj = Box::new(Ball {diameter: &num}) as Box<Red>;
}

以上代码能编译通过,因为生命周期和trait对象必须遵守:

  • trait对象默认的生命周期为'static
  • 若有&'a X&'a mut x,则默认生命周期为'a
  • 如有T: 'a从句,则默认生命周期为'a
  • 若有多个类似T: 'a从句,则需明确指定trait对象生命周期, Box<Red + 'a>Box<Red + 'static>

    todo

正如其他bound,任何Redtrait的实现内部包含引用,必须拥有和 trait对象bound中所指定的相同的生命周期