本文是对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 = ℞
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, 我们必须满足
变量v的生命周期必须包含&v的生命周期
&v的生命周期必须包含r的生命周期
这两种情况任意一个不满足都会产生悬空指针。
在我看来r是v的一个引用,只需要满足v的生命周期包含r的生命周期即可
引用作为函数入参
一个带lifetime的函数是这样子的
这里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(¶bola);
}
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的引用是一个根本又复杂的话题,这里还有很多东西没有讨论清楚, 比如强制类型转换, 比如重借用等等, 后续还需要进一步的研究