rust 所有权

 

所有权是rust独特而核心的内容,但它到底讲了啥,有什么用?让我们来看看

所有权是rust独特而核心的内容,但它到底讲了啥,有什么用?让我们来看看

所有权的目的是为了内存安全而发明的,内存安全包括如下内容:

  • 防止内存不安全
    • 编译器语法检查
      • rust 不允许使用未定义内存,在编译阶段就不会通过
      • 缓冲区溢出
        • 编译期和运行期都有边界检查
    • 所有权
      • 非法操作内存
      • 空指针
      • 悬垂指针

所有权机制

rust的所有权可以保证内存安全和性能。性能主要来自对数据的复制是浅拷贝(拷贝栈内存),即只在栈上拷贝或移动指向数据(堆内存)的指针;而不是深拷贝,即拷贝保存在堆上的大量具体数据。

rust中的“赋值”,默认是值的剪切以及所有权的剪切。所有权可以理解成数据或内存的增删改查权。比如

let a = Box::new(5);
let b = a;

类似于C++

unique_ptr<int> a(new int(5));
auto b = move(a)
    +---+---+---+---+---+   +---+---+---+---+---+
栈  |   | a |   | b |   |   |   | a |   | b |   |
    +---+-+-+---+---+---+   +---+---+---+-+-+---+
          |                               |
          +---+                       +---+
              |                       |
        +---+-v-+---+           +---+-v-+---+
堆      |   | 5 |   |           |   | 5 |   |
        +---+---+---+           +---+---+---+

一个值的所有权被转移给另外一个变量绑定的过程,就叫作所有权转移

所有者的权力

所有者是指对资源(包括内存、文件等)的管理者,它拥有:

  • [删] 控制资源的释放,即资源的删除权
  • 出借所有权
    • [改] 出借资源的改写权
    • [查] 出借资源的咨询权
  • 转移所有权

Copy与Clone

变量

对于 let y = x;

  • 如果 x 的类型实现了Copy特性,那么let y = x;就是按位将x复制给y
  • 如果 x的类型没有实现Copy特性,那么let y = x;就是移动,把x的值和所有权剪切给了y,x变成未初始化的变量
    • 即使x的类型实现了Clone特性,let y=x也是实行的移动(剪切)
    • 但可以用let y = x.clone()来显式的采用复制

结构体与枚举体

自定义的结构体都没有实现Copy特性,我们可以手动实现

#[derive(Copy, Clone)]
struct A {
  a: i32,
  b: i32,
}

结构体中的每一个元素都必须已经实现了Copy,才能给自定义结构装上Copy特性

元组与数组

对于元组类型来说,其本身实现了Copy,如果元素均为复制语义类型,则默认是按位复制的,否则会执行移动语义

let a = ("hello".to_string(), "world".to_string());
let b = a;

因为a中的元素都是移动类型,所以b = a执行的是移动语义

let a = (1, 2, 3);
let b = a;

因为a中的元素都是复制类型,所以b = a执行的是复制语义

数组和Option类型与元组类型都遵循这样的规则:如果元素都是复制语义类型,也就是都实现了Copy,那么它们就可以按位复制,否则就转移所有权

绑定、作用域与生命期

绑定

let a = "hello".to_string();
let b = a;
  • 标识符 aString 类型字符串"hello"通过 let 绑定在了一起,let绑定了标识符a和存储字符串"hello"的那块内存,从而a对那块内存拥有了所有权。
  • let b = a,相当于让b绑定a的那块内存,让a废弃

不可变与可变

  • rust绑定默认为不可变
    • 也就是let a = 5a不可以改变绑定的内存
  • rust的可变绑定用mut关键字
    • let mut a = 5mut是修饰a的,即mut a可以改变绑定的内存

生命期

生命期就是一个变量的作用域

  1. let 创建的词法作用域
      let a = "hello";
      let b = "world";
    

    rust 创建的词法作用域如下:

      'a { let a = "hello"; 
     'b { let b = "world"; 
     }
      }
    
    • 每个 let 声明都会创建一个默认的词法作用域,该作用域就是它的生命期
    • 当绑定在词法作用域中传递的时候,就会产生所有权的转移
    • let绑定会创建新的词法作用域,如果有其他变量作为右值进行赋值操作,那么该变量因为进入了 let 创建的词法作用域,所以要么转移所有权,要么按位复制
  2. 花括号创建词法作用域
  3. match创建词法作用域
      let a = Some("hello".to_string());
      match a {
     ...
      }
    
    • match a时,a进入了match的词法作用域,因为会执行 move
    • 绑定a是Option<String>类型,String类型具有移动语义,所以Option<String>就是移动语义类型
  4. 循环语句
    • for、loop以及while循环语句均可以创建新的作用域
       let v = vec![1, 2, 3];
       for i in v {
         ...
       }
      

      绑定v为移动语义类型,进入for循环时已经转移了所有权

  5. if let块和while let
    • if let块会创建一个新的作用域
  6. 函数
  7. 闭包捕获环境变量有三种方式
    1. 对复制语义类型,以不可变引用(&T)来捕获
    2. 对于移动语义类型,执行移动语义(move)转移所有权来捕获
       let s = "hello".to_string();
       let join = |i: &str| s + i;
       asset_eq!("hello, world", join(" world"));
      
    3. 对于可变绑定,如果在闭包中包含对其进行修改的操作,则以可变引用(&mut)来捕获

引用

  • 引用(Reference)是 Rust 提供的一种指针语义。
  • 引用是基于指针的实现,它与指针的区别是
    • 指针保存的是其指向内存的地址,
    • 引用可以看作某块内存的别名(Alias)
      • 使用它需要满足编译器的各种安全检查规则。
  • 引用分为
    • 可变引用&mut
    • 不可变引用&
  • 引用会限制所有者
    • 在不可变引用&期间,
      • 所有者不能修改资源
      • 并且也不能再进行可变借用
    • 在可变借用期间
      • 所有者不能访问资源
      • 并且也不能再出借所有权
  • 借用规则
    • 借用的生命周期不能长于出借方(拥有所有权的对象)的生命周期
    • 不能有多个可变借用(唯一可写)
    • 不可变借用(引用)不能再次出借为可变借用(可写不可读)

同一时刻,只能拥有一个写锁,或者多个读锁

               +-----------------+
               |                lock  +--read +-->
+--- write------>    memory     lock  +--read +-->
               |                lock  +--read +-->
               +-----------------+

               +-----------------+
               |                +----- read ----->
+--- write--->lock   memory     +----- read ----->
               |                +----- read ----->
               +-----------------+
fn compute(input: &u32, output: &mut u32) {
  let cache_input = *input;
  ...
}

优化:因为 input 是一个不可变借用,永远都不会改变,所以编译器可以将它的值保存在寄存器中,进一步提升性能

Rust的借用检查带来了如下好处:

  • 不可变借用保证了没有任何指针可以修改值的内存,便于将值存储在寄存器中。
  • 可变借用保证了在写的时候没有任何指针可以读取值的内存,避免了脏读
  • 不可变借用保证了内存不会在读取之后被写入新数据
  • 保证了不可变借用和可变借用不相互依赖,从而可以对读写操作进行自由移动和重新排序

解引用操作会获得所有权

let s: &String = &"hello".to_string();
let a = *s;

let a = *s会把String类型转移给a

生命期

生命期用于rust的借用检查器推断借用的合法性,rust要确保左右的借用都是有效的,防止产生悬垂指针。

rust中变量的生命期长度用'a表示,只有在出借方的生命期大于借用方的生命期长度,才不会造成悬垂指针。

显式生命期参数

生命期参数格式形如:'a,位于&之后

函数签名中的生命期参数

fn foo<'a>(s: &'a str, t: &'a str) -> &'a str;

函数或方法参数的生命期叫作“输入生命期”,返回值的生命期称为“输出生命期”,输出生命期 < 输入生命期。

fn the_longest<'a, 'b:'a>(s1: &'a str, s2: &'b str) -> &'a str;
                      's1  's2
                       ^    ^
                       |    |
                       +    +
let res = the_longest(&s1, &s2);
   +-+-+  +---------+---------+
     |              |
     v              v
    'r             't
  • the_longest 函数的第一个参数是 &s1,我们将其生命期命名为's1the_longest函数签名中的泛型参数'a在调用时被单态化为了's1
  • 第二个参数&s2的生命周期我们将其命名为's2。同理,可以说,泛型参数'b在此时被单态化为了's2
  • the_longest(s1_r,&s2)返回引用的生命周期记为't
  • let声明了绑定res,其生命周期记为'r
    • 因为 res 绑定了 the_longest 函数返回的借用,本质上是引用的按位复制,所以res成为了借用方,其生命周期长度为'r,且等于't

生命期的目的

生命周期参数的目的是帮助借用检查器验证合法的引用,消除悬垂指针

结构体定义中的生命周期参数

结构体在含有引用类型成员的时候也需要标注生命周期参数

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

结构体实例的生命周期应短于或等于任意一个成员的生命周期:L(Foo) < L(Foo.part)

let f = Foo {part: frist};

first实例化结构体Foo时,编译器就会根据结构体定义的生命期规则对其成员part的生命周期长度进行检查。要求L(f) < L(Foo.part),满足这个检查即可保证没有悬垂指针

方法定义中的生命周期参数

为带生命期参数的结构体实现方法,方法也要带上生命期参数

impl<'a> Foo<'a> {
  ...
}

静态生命周期参数

作静态生命周期。'static生命期存活于整个程序运行期间。所有的字符串字面量都有'static生命期,类型为 &'static str。全局静态类型,它的数据和程序代码一起存储于可执行文件的数据段中,其地址在编译期是已知的,并且是只读的,无法更改

let a = "hello, world";
let b = a; // 此处赋值执行的是按位复制,而非移动语义

因为静态字符串按位复制的仅仅是存储于栈上的地址,因为数据段是只读的,并不会出现什么内存不安全的问题

省略生命周期参数

Rust针对某些场景确定了一些常见的模式,将其硬编码到Rust编译器中,以便编译器可以自动补齐函数签名中的生命周期参数,这样就可以省略生命周期参数。被硬编码进编译器的模式称为生命周期省略规则(Lifetime Elision Rule),一共包含三条规则:

  1. 每个输入位置上省略的生命周期都将成为一个不同的生命周期参数
  2. 如果只有一个输入生命周期的位置(不管是否忽略),则该生命周期都将分配给输出生命周期
  3. 如果存在多个输入生命周期的位置,但是其中包含着&self&mut self,则self的生命周期都将分配给输出生命周期

生命周期限定

  • T:'a,表示T类型中的任何引用都要“活得”和'a一样长
  • T:Trait+'a,表示T类型必须实现Trait,并且T类型中任何引用都要“活得”和'a一样长

trait对象的生命周期

一个trait对象中实现trait的类型带有生命周期参数

trait Too{};
struct Boo<'a> {
  x: &'a i32,
}
impl<'a> Too for Boo<'a>{}

fn main() {
  let num = 5;
  let box_boo = Box::new(Boo{x :&num});
  let trait_obj = box_boo as Box<Too>;
}

trait对象默认遵循的生命周期规则:

  • trait对象的生命周期默认是'static
  • 如果实现trait的类型包含&'a x&'a mut x,则默认生命周期就是'a
  • 如果实现trait的类型只有 T:'a,则默认生命周期就是'a
  • 实现trait的类型包含多个类似T:'a的从句,则生命周期需要明确指定

智能指针与所有权

智能指针拥有资源的所有权,普通引用只是对所有权的借用

Box<T>智能指针也可以使用解引用操作符进行解引用,比如:

let a = Box::new("hello");
let b = Box::new("hello".to_string());
let a1 = *a; // 之后a还可以继续访问
let b1 = *b; // 之后b为未初始化状态

智能指针可以解引用是因为它们实现了Deref方法,返回的是&T类型。所以*a相当于*(a.deref), *b相当于 *(b.deref)

这种对Box<T>使用操作符*进行解引用而转移所有权的行为,被称为解引用移动,未来会有使用trait DerefMove定义此行为。注意Rc<T>Arc<T>不支持解引用移动。比如:

let a = Rc::new("hello");
let a1 = *a; // 这是不允许的
// 错误:不能把内容移出容器

Box<T>和其他智能指针相同的地方在于内部都使用了box关键字来进行堆分配,但它代表所有权唯一的智能指针

box关键字会调用内部堆分配方法exchange_malloc和堆释放方法box_free进行堆内存管理

共享所有权Rc和Weak

Rc<T>引用计数容器,

  • 可以把所有权共享给多个变量
  • 用于希望共享堆上分配的数据可以供程序的多个部分读取的场景
  • 并且确保共享的资源析构函数都能被调用到,借用&T不会调用析构函数
  • x.clone()得到强引用,即析构权被复制了
  • Rc::downgrade(&x)得到的是弱引用,它共享的智能指针没有析构权
  let x = Rc::new(5);
  let x1 = x.clone();
  let x2 = x.clone();
  // strong count: 3
  println!("strong_count: {:?}", Rc::strong_count(&x));
  let w = Rc::downgrade(&x);
  // strong count: 1
  println!("weak_count: {:?}", Rc::weak_count(&x));
  let y = &*x;
  // strong count: 3
  println!("strong_count: {:?}", Rc::strong_count(&x));
  // strong count: 1
  println!("weak_count: {:?}", Rc::weak_count(&x));

内部可变性Cell和RefCell

  • Rust中的可变不可变主要是针对一个变量绑定而言的。
  • 对于结构体,可变不可变只能对其实例进行设置,不能设置单个成员的可变性
  • 实际中,某个字段可变而其他字段不可变的情况也存在
  • Rust提供了Cell<T>RefCell<T>来应对
  • 它们本质上只是可以提供内部可变性(Interior Mutability)的容器

Cell

内部可变性容器是对Struct的一种封装,表面不可变,但内部可以通过某种方法来改变里面的值

let x = Cell::new(5);
assert_eq!(x.get(), 5);
x.set(10);
assert_eq!(x.get(), 10);
  • 对于Cell<T>中的类型T,只有实现了Copy的类型T,才可以使用get方法获取包裹的值,因为get方法返回的是对内部值的复制
  • 对于没有实现Copy的类型T,则提供了get_mut方法来返回可变借用
  • 任何类型T都可以使用set方法修改其包裹的值

RefCell

  • RefCell<T>borrow/borrow_mut方法,对应Cell<T>的get/set`方法
  • RefCell<T>有运行时开销,因为它自己维护着一个运行时借用检查器
  • Cell<T>RefCell<T>之间的区别
    • 读写
      • Cell<T>使用set/get方法直接操作包裹的值
      • RefCell<T>通过borrow/borrow_mut返回包装过的引用Ref<T>RefMut<T>来操作包裹的值
    • CopyMove
      • Cell<T>一般适合复制语义类型(实现了Copy)
      • RefCell<T>一般适合移动语义类型(未实现Copy)
    • 运行时开销
      • Cell<T>无运行时开销
      • RefCell<T>需要在运行时执行借用检查,所以有运行时开销
    • 引发panic
      • Cell<T>永远不会在运行时引发panic
      • RefCell<T>一旦发现违反借用规则的情况,则会引发线程panic而退出当前线程

写时复制Cow

写时复制(Copy on Write)技术是一种程序中的优化策略,只有等到真正需要写入的时候才复制。

Cow<T>是一个枚举体的智能指针,包括两个可选值:

  • Borrowed,用于包裹引用
  • Owned,用于包裹所有者
  • Option<T>表示的是值的“有”和“无”,而 Cow<T>表示的是所有权的“借用”和“拥有”

Cow<T>提供的功能是

  • 不可变的方式访问借用内容
    • Cow<T>实现了Deref,所以可以直接调用其包含数据的不可变方法
  • 在需要可变借用所有权的时候再克隆一份数据
    • 借用的数据被克隆为的新的对象
let s1 = [1, 2, 3, 1, 2];
let mut i1 = Cow::from(&s1[..]);
abs_all(&mut i1);

因为所有值都大于0,没有变化,所以这里没有发生数据的复制

let s2 = [1, 2, 3, -1, 2];
let mut i2 = Cow::from(&s2[..]);
abs_all(&mut i2);

因为s2[3] = -1, abs_all会修改内容,所以这里发生了复制

let s3 = Cow::from(vec![1, 2, 3, -1, 2]);
abs_all(&mut s3);

因为数据本身拥有所有权,所以不会复制

使用Cow时,需要掌握以下几个要点:

  • Cow<T>实现了Deref,所以可以直接调用T的不可变方法
  • 在需要修改 T 时,可以使用 to_mut 方法来获取可变借用
    • 该方法会产生克隆,但仅克隆一次,如果多次调用,则只会使用第一次的克隆对象
    • 如果T本身拥有所有权,则此时调用to_mut不会发生克隆
  • 在需要修改T时,也可以使用into_owned方法来获取一个拥有所有权的对象
    • 如果T是借用类型,这个过程会发生克隆,并创建新的所有权对象
    • 如果T是所有权对象,则会将所有权转移到新的克隆对象

并发安全与所有权

Rust的两个trait——SendSync

  • 如果类型 T 实现了 Send,说明该类型的实例可以在线程间安全传递所有权
  • 如果类型T实现了Sync,表明该类型的实例在多线程并发中可以安全地跨线程共享

rust 线程安全的同步机制:

  • Arc<T>是线程安全版本的Rc<T>
  • Mutex<T>是锁,同一时间仅允许有一个线程进行操作
  • RwLock<T>相当于线程安全版本的RefCell<T>,可以同时运行多个reader或者一个writer
  • Atomic 系列类型相当于线程安全版本的Cell<T>

非词法作用域生命周期

非词法作用域生命周期(Non-Lexical Lifetime,NLL),基于MIR的借用检查

MIR是基于控制流图(Control Flow Graph,CFG)的抽象数据结构,基于MIR的借用检查称为非词法作用域的生命周期