Rust Shared And Mutable References

本文是对Programming Rust 第5章的总结,部分代码与图片来自此书

共享引用与可变引用

虽然可以同时存在多个共享引用,但是当共享引用存在的时候,不但不能够存在可变引用,甚至你也不能修改或者move 拥有者本身,比如

1
2
3
4
let v = vec![4, 8, 19, 27, 34, 10];
let r = &v;
let aside = v; // move vector to aside
r[0]; // bad: uses `v`, which is now uninitialized

上面的v 在被引用的时候move给了r, 这种操作是编译器禁止的

一个场景是在编程当中经常出现的,比如你需要复制字符串自身,使用rust,你可能会这样实现

1
2
3
4
5
6
7
8
fn extend(vec: &mut Vec<f64>, slice: &[f64]) {
 for elt in slice {
 vec.push(*elt);
 }
}

let wave = vec![0.0, 1.0];
extend(&mut wave, &wave);

但是这种操作是被编译器禁止的,由于修改wave的时候很可能改变它的内存地址,比如扩充大小,这个时候slice指向 了已经被摧毁的内存地址,程序就会出现访问无效内存的问题,以下是示意图

这些编译错误使我们重新看一下rust的引用规则

  • 共享访问

    被共享引用的值是只读的,在共享引用的周期内, 这个值不能被改变,即使是通过变量本身也不行

  • 排他访问

​ 可变引用是排他的, 在可变引用周期内, 这个值甚至不能被访问,唯一的例外就是可以reborrow这个可变引用

一下是一些体现这些规则的例子

共享引用期间不能修改

1
2
3
4
    let mut y = 10;
    let r = &y;
    y = 11;  //can not change y
    println!("{}", r);

可变引用期间不能问

1
2
3
4
5
6
   //can not use of mut borrowed y, even it is copy type
      let mut y = 10;
      let r = &mut y;
      
      let z = y;  //can not access y
      print!("{}, {}", r, z);

从共享引用reborrow

1
2
3
4
5
 let mut w = (107, 109);
    let r = &w;
    let r0 = &w.0;

    println!("{:?}, {}", r, r0);

从可变引用reborrow

1
2
3
4
5
6
7
8
9

  let mut w = (100, 101);
    let m = &mut w;
    let m0 = &mut m.0; //reborrow mutable from m
    // let m0 = &mut w.0; //not possible, can only reborrow from m
    let r1 = &m.1;  //reborrow shared from m

    // let v = w.0; //can not accessed, because of mut borrowed by m
    print!("{:?}, {}",r1, m0);

这些限制可能看起来很奇怪,也会限制灵活性。 但是能排除一些安全隐患, 最典型的就是同时操作自身的例子,比如操作一个文件,我们很可能会复制自身

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
struct File {
 descriptor: i32
}
fn new_file(d: i32) -> File {
 File { descriptor: d }
}
fn clone_from(this: &mut File, rhs: &File) {
 close(this.descriptor);
 this.descriptor = dup(rhs.descriptor);
}


let mut f = new_file(open("foo.txt", ...));
...
clone_from(&mut f, &f);

如果以上代码能够执行,那么显然跟我们预期不太一样, 一个我们希望复制的文件已经被关闭了

这些现在也会在并发编程的时候带来一些好处, 因为并发中的竞争就是因为多个线程对于同个变量的修改导致的。但是rust中并不允许同时存在多个可变引用。

对象海

垃圾回收机制的引入通常导致了对象海的产生,开发者会在代码中将各个对象互相引用, 这样做可能会提高一点开发效率,但是由于缺乏良好的设计,随着代码量的增加,系统很快就会变成一团乱麻,难以维护, rust严格的所有权机制会使开发者提前思考系统设计,写出更好,更加可维护的程序

Rust References

本文是对Programming Rust 第5章的总结,部分代码与图片来自此书

引用类型

rust有三种指针类型

  • 引用
  • 裸指针
  • 智能指针

其中引用是一种非拥有指针,即不需要对被引用对象的生命周期负责,相反,引用的生命周期不能超过被引用者的生命周期

很多时候我们并不想转移值的所有权,仅仅需要访问值的某些数据,这个时候就需要使用引用了。

引用分为两种类型:

  • 共享引用 可以读取被引用对象但是不能修改。同时可以存在多个共享引用,共享引用本身是Copy类型
  • 可变引用可以读取并且修改被引用对象,同时只能存在一个可变引用, 并且可变引用存在的时候, 不能存在任何共享引用,可变引用不是Copy类型

需要说明的是, 可变引用存在的时候, 不但是共享引用不能存在, Owner自己也不能访问和修改被引用对象

使用引用

以下是使用引用的一些示例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
//共享引用
let x = 10;
let r = &x;
assert!(*r == 10);

//可变引用
let mut y = 32;
let m = &mut y;
*m += 32;
assert!(*m == 64);

隐式解引用/隐式借引用

这里需要注意的是由于解引用,借引用是非常常见的操作,而且编码起来显得非常繁琐,所以rust对.操作符的左值做了隐式解引用与隐式借引用的操作

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
//隐式解引用, 接引用
struct Anime {
	name: String,
    bechdel_pass: bool,
};
let aria = Anime {
    name: String::from("Aria: The Animation"),
    bechdel_pass: true,
};

//隐式借引用
aria.print();
(&aria).print();

let anime_ref = &aria;
//隐式解引用
println!("Hello, {}!", anime_ref.first_name);
println!("Hello, {}!", (*anime_ref).first_name);

嵌套引用

rust中引用本身也是一个左值, 所以可以对引用再引用,比如

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
struct Point {
	x: i32,
    y: i32,
}
let point = Point { x: 1000, y: 729 };
let r: &Point = &point;
let rr: &&Point = &r;
let rrr: &&&Point = &rr;

//*操作符 循环解引用
let rr0:&&Point = *rrr;
let r0: &Point = *rrr;
//.操作符循环解引用,直到到对应的值
let y: 32 = rrr.y;

有个值得注意的点是在The book中有一段是这么说的

Note that the * operator is replaced with a call to the deref method and then a call to the * operator just once, each time we use a * in our code

上面的代码表面,对于引用,一个*操作符可以直接解引用到多个层级之上的类型。

我认为这是由于当我指定了类型之后, rust编译器会根据目标类型进行Deref Coercion, 我姑且叫它强制类型转换,强制类型转换会无限的将一个引用转成它可以转换到的引用,如果一个类型符合 T: Deref<Target=U>

引用比较/计算

引用比较的是他们背后的值, 引用只能在相同类型直接做比较

引用运算可以计算一层引用的背后的值, 多层引用则不能相加

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
let x = 10;
let y = 10;
let rx = &x;
let ry = &y;
let rrx = &rx;
let rry = &ry;
assert!(rrx <= rry);
assert!(rrx == rr
assert!(rx == ry); // their referents are equal 
assert!(!std::ptr::eq(rx, ry)); // but occupy different addresses

//引用相加
let n = rx + ry;  
assert_eq!(n, 20);
let m = rrx + rry; //编译不通过, 这两个变量不能相加

胖指针

rust有两种常见的胖指针

  • 对slice的引用, 额外包含length信息
  • 对trait object的引用, 额外包含trait实现地址,即虚表

胖指针名字的来源是因为这种指针包含两个字长,一般的指针只包含一个。

指向unszie type的指针都是胖指针

引用安全

引用在rust中是内存安全的, 引用不会产生悬空指针的问题,rust通过lifetime来解决这个问题,以下这个段代码展示了违背生命周期的情况

1
2
3
4
5
6
7
8
{
    let r;
    {
        let x = 1;
        r = &x;
    }
    assert_eq!(*r, 1); // bad: reads memory `x` used to occupy
}

x在引用r之前被释放了, 这种情况是无法通过编译的, rust对于生命周期的限制如下:

假如我们有一个变量v的引用&v, 使变量r=&v, 我们必须满足

  1. 变量v的生命周期必须包含&v的生命周期

  2. &v的生命周期必须包含r的生命周期

这两种情况任意一个不满足都会产生悬空指针。

在我看来r是v的一个引用,只需要满足v的生命周期包含r的生命周期即可

引用作为函数入参

一个带lifetime的函数是这样子的

1
fn f<'a>(p: &'a i32){}

这里f<'a> ‘a是作为函数的参数存在, 我们可以认为是 对于任意生命周期都适用,大部分情况下, 一个实参的周期是大于函数调用的。但是如果函数中存在这种情况,就不行了

1
2
3
4
5
6
static mut STASH: &i32 = &128;
fn f(p: &i32) {
    unsafe{
        STASH = P;
    }
}

STASH的生命周期是全局的, 而根据上面的规则2,p的生命周期必须包含’static。显然对于任意生命周来说这是不满足的。这个时候只有将形参改成’static才能满足这个条件

1
2
3
4
5
fn f(p :&'static i32){
      unsafe{
        STASH = P;
    }
}

以上的函数是正确的, 但是适用范围也更小了。

引用作为函数返回

以下是一个返回引用的函数

1
2
3
4
5
6
7
8
9
fn smallest(v: &[i32]) -> &i32 {
   let mut s = &v[0];
   for r in &v[1..] {
       if *r < *s {
           s = r;
       }
   }
   s
}

我们忽略了生命周期, 当一个函数只有一个引用作为参数的时候,如果它的返回值包含引用, rust会认为它们有同样的生命周期。现在考虑以下的代码

1
2
3
4
5
6
7

let s;
{
    let parabola = [9, 4, 1, 0, 1, 4, 9];
    s = smallest(&parabola);
}
assert_eq!(*s, 0);

根据上面的规则1,2, 必须满足parabola的生命周期必须大于s的生命周期, 这段代码中parabola要比s更早drop,所以是不能通过编译的

结构体包含引用

结构体必须包含生命周期参数,不然是无法通过编译的

1
2
3
4
5
6
7
8
//含引用结构体
struct S<'a> {
    r: &'a i32
}
//嵌套含引用结构体
struct D<'a> {
    s: S<'a>
}

省略生命周期参数

省略生命周期有以下几种情况:

  • 函数不返回引用的情况下, 不需要显示指定生命周期
  • 如果函数只有一个参数,那么也不需要指定生命周期, 因为rust为认为任何返回引用的生命周期都与此参数相同
  • 如果函数是一个类型的方法,并且形参是引用类型self(&self, &mut self), rust会认为返回引用的生命周期与此相同

后续

rust的引用是一个根本又复杂的话题,这里还有很多东西没有讨论清楚, 比如强制类型转换, 比如重借用等等, 后续还需要进一步的研究

Rust Ownership And Move

本文是对Programming Rust 第4章的总结,部分代码与图片来自此书

背景

我们通常希望变成语言提供以下特性:

  • 我们希望可以控制内存释放的时机
  • 我们不希望使用一个对象已经被释放的指针

c/c++与带gc的语言通过不同的机制解决以上问题,但是都各自有不同的缺点 rust使用了Ownership与Borrow来解决内存管理的问题

Ownership

  • 在rust中一个变量被赋值的时候就拥有了这个值的所有权,一个Struct拥有字段的所有权, 一个Tuple,Array,Vec拥有包含元素的所有权。
  • 一个值只能被一个变量拥有, 这个变量会对此值的生命周期负责, 当一个变量被Drop的时候 它所拥有的所有值也会一起被销毁

以下代码创建了一个Vec, 它包含了多个结构体

 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
27
28
29
30
31
32
33
34
35
36
37
38
39
40

 struct Person {

 name: String,

 birth: i32,

 }

 let mut composers = Vec::new();

 composers.push(Person {

 name: "Palestrina".to_string(),

 birth: 1525,

 });

 composers.push(Person {

 name: "Dowland".to_string(),

 birth: 1563,

 });

 composers.push(Person {

 name: "Lully".to_string(),

 birth: 1632,

 });

 for composer in &composers {

 println!("{}, born {}", composer.name, composer.birth);

 }

它的内存布局为

在很多情况下,这种一对一的绑定关系显然缺乏灵活性, 在真实的编程当中,对象之间的互相引用,多个对象引用同一个对象是很常见的(比如双向链表, 树结构等等)。所以rust在所有权原则的基础上提供很多其他机制来达成这种灵活性

Move

在rust中,像赋值,对函数传值,函数返回值等操作会使一个变量转移它对值得所有权,在一个变量转移所有权之后, 它会变成未初始化状态, 后续得代码无法访问该变量。比如以下代码

1
2
3
4
5
6

 let s = vec!["udon".to_string(), "ramen".to_string(), "soba".to_string()];

 let t = s;

 let u = s;

在内存中的布局如下所示

  • s在把值转移给t后变为未初始化状态,同时t只是对s的值本身(Value Proper)做了赋值。这里的Value Proper我理解的意思就是,大部分情况就是一个值的栈上数据, 因为栈上数据不需要做内存管理;当然肯定存在堆上数据做Move的情况,总的来说编译器会做一次浅复制
  • Move操作保证了一个值始终只有一个拥有者,所以有且只有一个人会对这个值的内存管理负责

控制流中的Move

rust编译器会根据条件语句来判断,一个值是否被Move,比如

if/else语句

1
2
3
4
5
6
7
let x = String::from("hello, move");
if c {
	f(x);  //这个move允许
}else {
	g(x);  //这个move也允许
}
h(x); //这里无论哪种情况move都已经发生

循环语句

1
2
3
4
let x = String::from("hello, move");
while c {
	g(x);  //这里编译不会通过,因为x会再第一次move的时候失效
}

数组中的Move

一个Vec V中的元素属于V, 不能将他Move给其他变量

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14

 // Build a vector of the strings "101", "102", ... "105"

 let mut v = Vec::new();

 for i in 101..106 {

 v.push(i.to_string());

 }

 // Pull out random elements from the vector.

 let third = v[2]; // error: Cannot move out of index of Vec let fifth = v[4]; // here too
  • 当然了一个Vec必须可以删除元素, 比如移除队尾数据, 移除数据并且使用队尾填充, 还有著名的mem::replace, 这些方法都遵循一个原则就是保证vec在一个连贯的状态,使得Vec只在记录length的情况下就能维护本身的状态。 由于对外赋值会导致Move, 所以一般赋值操作可以使用Borrow或者Clone
 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
27
28
29
30
31
32
33
// Build a vector of the strings "101", "102", ... "105"

 let mut v = Vec::new();

 for i in 101..106 {

 v.push(i.to_string());

 }

 // 1. Pop a value off the end of the vector:

 let fifth = v.pop().expect("vector empty!");

 assert_eq!(fifth, "105");

 // 2. Move a value out of a given index in the vector,

 // and move the last element into its spot:

 let second = v.swap_remove(1);

 assert_eq!(second, "102");

 // 3. Swap in another value for the one we're taking out:

 let third = std::mem::replace(&mut v[2], "substitute".to_string());

 assert_eq!(third, "103");

 // Let's see what's left of our vector.

 assert_eq!(v, vec!["101", "104", "substitute"]);
  • 值得一提的是 ,一个Vec使用remove方法删除index处的元素时, 所有右边的元素都会左移,这个操作消耗是很大的。不过这个东西跟rust无关, 所有语言都是这么实现

  • 另外在一个for循环中, 我们会将整个Vec转移出去, 并且将他打散成各个元素, 后续不管这些元素有没有被再次转移, 整个初始Vec已经不可见了

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
let v = vec!["a".to_string(), "b".to_string(), "c".to_string()];

  

 for mut s in v {          //move the entire v

 if s == "b".to_string() {

 break;

 }

 }

  

 println!("{}", v[2])  //v is moved, 编译不通过

Copy

在rust中对于简单类型,也使用Move语义是没有必要的, 因为简单类型除了自己的值本身之外并没有要额外管理的资源,这个资源可能是堆内存,可能是文件句柄等。这个时候对一个变量赋值应该是直接Copy就可以了, 事实上他跟Move就仅仅是是否清除原变量的值本身,原变量是否保持可用

Copy类型包含了

  • 整数,浮点,字符,布尔,指针等
  • Copy类型组成的Tuple
  • Copy类型组成的Array
  • 指定了Copy的Struct
  • 指定了Copy的枚举
  • 所有复制只需要位拷贝的类型

不包含

  • 所有Drop的时候需要额外处理的都不是Copy类型
  • 默认的`Struct
  • 默认的Enum

Rc和Arc

有了Move跟Copy之后,值的使用仍然比较受限, 很多时候我们希望可以有多个变量持有同一个值,就像其他语言一样。这个时候Rc就派上用场了,一个Rc就是在遵循前面几种规则的基础上,创造出的一种引用计数类型。

  • 按照Ownership的原则, 是不允许多个变量同时持有一个值T的。那么怎么做呢?答案就是把值T包装到一个容器里, 容器本身遵循Ownership, 容器内部来决定何时drop关联的值T, 这里这个容器就是Rc.
  • Rc的共享使用Clone来实现, Clone操作只是对引用计数+1, 当变量被Drop时引用技术-1,当引用技术变为0的时候, Rc所包装的值T被释放
  • Rc所包装的值T是不可变的, 这个符合单个可变与多个共享之间的排斥性,某些情况下可以使用内部可变性容器来解决这个问题, 比如Cell
1
2
3
4
5
6

 let s: Rc<String> = Rc::new("shirataki".to_string());

 let t: Rc<String> = s.clone();

 let u: Rc<String> = s.clone();

以下是Rc的内存布局