[ Rust笔记 十二] 内存安全-借用和生命周期

生命周期

先看一个示例

1
2
3
4
5
6
7
8
fn main() {
let v = vec! [1,2,3,4,5]; // --> v 的生命周期开始
{
let center = v[2]; // --> center 的生命周期开始
println! ("{}", center);
} // <-- center 的生命周期结束
println! ("{:? }", v);
} // <-- v 的生命周期结束

借用

变量对其管理的内存拥有所有权。这个所有权不仅可以被转移(move)​,还可以被借用(borrow)。

借用指针的语法使用

  1. &符号只读借用
  2. &mut可读写借用

借用指针与普通指针的内部数据是一模一样的,唯一的区别是语义层面上的。它的作用是告诉编译器,它对指向的这块内存区域没有所有权。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
fn main() {
let mut var = 0_i32;
{
let p1 = &mut var; // p1 指针本身不能被重新绑定,我们可以通过p1改变变量var的值
*p1 = 1;
}
{
let temp = 2_i32;
let mut p2 = &var; // 我们不能通过p2改变变量var的值,但p2指针本身指向的位置可
以被改变
p2 = &temp;
}
{
let mut temp = 3_i32;
let mut p3 = &mut var; // 我们既可以通过p3改变变量var的值,而且p3指针本身指向
的位置也可以改变
*p3 = 3;
p3 = &mut temp;
}
}

借用指针在编译后,实际上就是一个普通的指针,它的意义只能在编译阶段的静态检查中体现。

借用规则

  1. 借用指针不能比它指向的变量存在的时间更长。
  2. &mut型借用只能指向本身具有mut修饰的变量,对于只读变量,不可以有&mut型借用。
  3. &mut型借用指针存在的时候,被借用的变量本身会处于“冻结”状态。
  4. 如果只有&型借用指针,那么能同时存在多个;如果存在&mut型借用指针,那么只能存在一个;如果同时有其他的&或者&mut型借用指针存在,那么会出现编译错误。

借用指针只能临时地拥有对这个变量读或写的权限,没有义务管理这个变量的生命周期。因此,借用指针的生命周期绝对不能大于它所引用的原来变量的生命周期,否则就是悬空指针,会导致内存不安全。示例如下:

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
// 这里的参数采用的“引用传递”,意味着实参本身并未丢失对内存的管理权
fn borrow_semantics(v : &Vec<i32>) {
// 打印参数占用空间的大小,在64位系统上,结果为8,表明该指针与普通裸指针的内部表示方法相同
println! ("size of param: {}", std::mem::size_of::<&Vec<i32>>());
for item in v {
print! ("{} ", item);
}
println! ("");
}
// 这里的参数采用的“值传递”,而Vec没有实现Copy trait,意味着它将执行move语义
fn move_semantics(v : Vec<i32>) {
// 打印参数占用空间的大小,结果为24,表明实参中栈上分配的内存空间复制到了函数的形参中
println! ("size of param: {}", std::mem::size_of::<Vec<i32>>());
for item in v {
print! ("{} ", item);
}
println! ("");
}

fn main() {
let array = vec! [1, 2, 3];
// 需要注意的是,如果使用引用传递,不仅在函数声明的地方需要使用&标记
// 函数调用的地方同样需要使用&标记,否则会出现语法错误
// 这样设计主要是为了显眼,不用去阅读该函数的签名就知道这个函数调用的时候发生了什么
// 而小数点方式的成员函数调用,对于self参数,会“自动转换”,不必显式借用,这里有个区别
borrow_semantics(&array);
// 在使用引用传递给上面的函数后,array本身依然有效,我们还能在下面的函数中使用
move_semantics(array);
// 在使用move语义传递后,array在这个函数调用后,它的生命周期已经完结
}

在这里给大家提个醒:一般情况下,函数参数使用引用传递的时候,不仅在函数声明这里要写上类型参数,在函数调用这里也要显式地使用引用运算符。但是,有一个例外,那就是当参数为self &self &mut self等时,若使用小数点语法调用成员方法,在函数调用这里不能显式写出借用运算符。以常见的String类型来举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
fn main() {
// 创建了一个可变的 String 类型实例
let mut x : String = "hello".into();
// 调用 len(&self) -> usize 函数。 self的类型是 &Self
// x.len() 等同于 String::len(&x)
println! ("length of String {}", x.len());
// 调用fn push(&mut self, ch: char) 函数。self的类型是 &mut Self,因此它有权对字符
串做修改
// x.push('! ') 等同于 String::push(&mut x, '! ')
x.push('! ');
println! ("length of String {}", x.len());
// 调用 fn into_bytes(self) -> Vec<u8> 函数。注意self的类型,此处发生了所有权转移
// x.into_bytes() 等同于 String::into_bytes(x)
let v = x.into_bytes();
// 再次调用len(),编译失败,因为此处已经超过了 x 的生命周期
//println! ("length of String {}", x.len());
}

任何借用指针的存在,都会导致原来的变量被“冻结”​(Frozen)​。示例如下:

1
2
3
4
5
6
fn main() {
let mut x = 1_i32;
let p = &mut x;
x = 2;
println! ("value of pointed : {}", p);
}

编译结果为

1
error: cannot assign to `x` because it is borrowed

生命周期标记

函数的生命周期标记

1
2
3
4
5
6
7
8
9
10
11
12
13
14
01| struct T {
02| member: i32,
03| }
04|
05| fn test<'a>(arg: &'a T) -> &'a i32
06| {
07| &arg.member
08| }
09|
10| fn main() {
11| let t = T { member : 0 }; //----- 't -|
12| let x = test(&t); //-- 'x ---| |
13| println! ("{:? }", x); // | |
14| } //-- 'x ----- 't -

生命周期之间有重要的包含关系。如果生命周期’a比’b更长或相等,则记为’a :’b,意思是’a至少不会比’b短​。对于借用指针类型来说,如果&’a是合法的,那么’b作为’a的一部分,&’b也一定是合法的。

‘static是一个特殊的生命周期,它代表的是这个程序从开始到结束的整个阶段,所以它比其他任何生命周期都长。这意味着,任意一个生命周期’a都满足’static :’a。

修改上述test函数,编译报错,因为’a和’b没有关系,没办法确定生命周期包含关系。

1
2
3
4
fn test<'a, 'b>(arg: &'a T) -> &'b i32
{
&arg.member
}

可以修改为

1
2
3
4
5
fn test<'a, 'b>(arg: &'a T) -> &'b i32
where 'a:'b
{
&arg.member
}

编译器可以把&x和&y的生命周期都缩小到某个生命周期’a以内,且满足’x : ‘a, ‘y : ‘a。返回的selected变量具备’a生命周期,也并没有超过’x和’y的范围。

1
2
3
4
5
6
7
8
9
10
11
12
13
fn select<'a>(arg1: &'a i32, arg2: &'a i32) -> &'a i32 {
if *arg1 > *arg2 {
arg1
} else {
arg2
}
}
fn main() {
let x = 1;
let y = 2;
let selected = select(&x, &y);
println! ("{}", selected);
}

类型的生命周期标记

1
2
3
struct Test<'a> {
member: &'a str
}
1
2
3
4
impl<'t> Test<'t> {
fn test<'a>(&self, s: &'a str) {
}
}

如果在泛型约束中有where T: ‘a之类的条件,其意思是,类型T的所有生命周期参数必须大于等于’a。要特别说明的是,若是有where T: ‘static的约束,意思则是,类型T里面不包含任何指向短生命周期的借用指针,意思是要么完全不包含任何借用,要么可以有指向’static的借用指针。

省略生命周期标记

省略的生命周期被自动补全的规则

  • 每个带生命周期参数的输入参数,每个对应不同的生命周期参数;
  • 如果只有一个输入参数带生命周期参数,那么返回值的生命周期被指定为这个参数;
  • 如果有多个输入参数带生命周期参数,但其中有&self、&mut self,那么返回值的生命周期被指定为这个参数;
  • 以上都不满足,就不能自动补全返回值的生命周期参数。
1
2
3
4
fn get_str(s: &String) -> &str {
println! ("call fn {}", s);
"hello world"
}

编译器会自动补全生命周期参数:

1
2
3
4
fn get_str<'a>(s: &'a String) -> &'a str {
println! ("call fn {}", s);
"hello world"
}