Rust笔记:生命周期与引用有效性

Posted by keys961 on January 17, 2020

0. Intro

Rust引用是有生命周期的,即引用有一个有效作用域

  • 一般而言,引用的生命周期是隐式推导的
  • 少部分需要使用泛型生命周期参数来注明

1. Revision: 悬垂引用

Rust有借用检查器,用于检查引用的在其所在的作用域中都是有效的。例如下面这个例子,就会报悬垂引用的错误:

1
2
3
4
5
6
7
8
9
{
    let r;
    {
        let x = 5; // x owns 5 
        r = &x; // r ref/borrows x
    }
    // 5 that x owns is freed, the r is dangling
    println!("r: {}", r);
}

但是,有时候,Rust没有获得足够的信息来检查生命周期时,会要求我们显式标注生命周期。下面的例子也会报错(编译器不知道返回的引用是来自x还是y):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
fn longest(x: &str, y: &str) -> &str {
    if x.len() < y.len() {
        y
    } else {
        x
    }
}
/*
error[E0106]: missing lifetime specifier
 --> src/main.rs:1:33
  |
1 | fn longest(x: &str, y: &str) -> &str {
  |                                 ^ expected lifetime parameter
  |
  = help: this function's return type contains a borrowed value, but the
signature does not say whether it is borrowed from `x` or `y`
*/

为了解决上面的问题,Rust引入泛型生命周期参数,定义引用间的关系,以让借用检查器可以进行分析。

2. 泛型生命周期注解

泛型生命周期注解的作用:

  • 描述多个引用间生命周期的关系
  • 不影响生命周期

这类注解的语法比较特别,其参数必须以'开头,例如'a,'b等。

通常一般都会写'a,如下所示:

1
2
3
&T // 引用
&'a T // 显式生命周期标记的引用
&a mut T // 显式生命周期标记的可变引用

不过,由于这类注解用于描述多个引用的生命周期的关系,所以作用于单个引用是没用的,应该作用于多个引用间。

假如ref1ref2都被标记了'a,那么这2个引用的生命周期会被编译器视作一样

2.1. 函数的生命周期注解

在1中的longest函数改成下面的,给参数和返回值标记'a生命周期,即可通过编译:

1
2
3
4
5
6
7
fn longest(x: &a str, y: &'a str) -> &'a str {
    if x.len() < y.len() {
        y
    } else {
        x
    }
}

在该函数中,编译器认为:相同标记的参数和返回值引用,它们拥有相同生命周期,且取它们重叠的/最短的

所以下面2个例子,一个能通过编译,一个不能通过,可看代码注释的解释:

  • 通过编译的例子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    
    fn main() { // main
        let string1 = String::from("long string is long");
      
        {// inner-scope
            let string2 = String::from("xyz");
            // string1.as_str()引用生命周期在main
            // string2.as_str()引用生命周期在inner-scope
            // 函数中它们和返回值被标记生命周期'a
            // 所以,编译器认为它们以及返回值result的生命周期在inner-scope
            let result = longest(string1.as_str(), string2.as_str());
            // 所以这里是result是有效的,编译通过
            println!("The longest string is {}", result);
        }
    }
    
  • 不能通过编译的例子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    
    fn main() { // main
        let string1 = String::from("long string is long");
        let result;
        { // inner-scope
            let string2 = String::from("xyz");
            // string1.as_str()引用生命周期在main
            // string2.as_str()引用生命周期在inner-scope
            // 函数中它们和返回值被标记生命周期'a
            // 所以,编译器认为它们以及返回值result的生命周期在inner-scope
            result = longest(string1.as_str(), string2.as_str());
        }
        // 但这里result在main中,脱离了inner-scope,引用无效/悬垂,编译失败
        println!("The longest string is {}", result);
    }
    
    1
    2
    3
    4
    5
    6
    
    fn longest<'a>(x: &str, y: &str) -> &'a str {
        let result = String::from("really long string");
        // 这里返回引用的生命周期最多只能在longest内(取最小),即'a为longest
        // 那么调用该函数后,返回值将成为悬垂引用,编译失败
        result.as_str()
    }
    

2.2. 方法的生命周期注解

方法的生命周期注解需要在impl开头注明出来,如下所示:

1
2
3
impl<'a> T { // &self and &mut self are marked with 'a
    // method implementations ...
}

有了上面impl'a注解,方法中的&self以及&mut self参数都会有'a的注解;而且,若返回值是引用,默认也会有一样的生命周期注解。

方法中其它的参数和返回值请参考2.1.,规则一样适用。

2.3. 结构的生命周期注解

若结构体的字段是引用,那么这个引用就需要生命周期注解(否则编译器就无法保证结构存活时引用的的有效性)。

结构体中的生命周期注解是为了:保证引用在结构体存活时有效

例如下面的例子,引用partExample生命周期内有效:

1
2
3
struct Example<'a> {
    part: &'a str // 编译器保证part在Example生命周期内有效
}

2.4. 静态生命周期

可使用‘static标注一个引用是静态的,其生命周期能够存活于整个程序运行期间。

例如,所有字符串字面值都有静态生命周期,即:

1
let s: &'static str = "str";

3. 生命周期省略规则

对一些特殊场景,编译器会允许我们省略生命周期注解。它满足一定的规则。

不过在这之前先明确2个定义:

  • 输入生命周期:参数的生命周期
  • 输出生命周期:返回值的生命周期

编译器有3条生命周期省略规则,其应用于fnimpl块:

  • 每个是引用的参数都有自己的生命周期注解,若不指定,它们的生命周期注解互不相同,即:

    1
    2
    3
    4
    5
    
    /*
     fn func(a: &T) <==> fn func<'a>(a: &'a T)
     fn func(a: &U, b: &V) <==> fn func<'a, 'b>(a: &'a T, b: &'b T)
     以此类推
    */
    
  • 若输入的引用参数只有1个,且只有1个生命周期注解,则返回引用的生命周期注解和参数的一样,即:

    1
    2
    3
    4
    
    /*
     fn func(a: &T) -> &U <==> fn func(a: &'a T) -> &U  // Rule 1
     		<==> fn func(a: &'a T) ->&'a U // Rule 2
    */
    
  • 对于方法,若参数有很多生命周期注解,但由于有&self/&mut self参数的缘故,返回引用的生命周期注解和self一样,即:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    
    /*
     impl<'a> T {
     	 fn func(&self, arg: &U) -> &V {
             // ...
         }    
     }
    等价于:
     impl<'a> T {
     	 fn func(&'a self, arg: &'b U) -> &'a V { // Rule 1 and Rule 3
     	 	// self生命周期注解为'a,它和返回引用的生命周期注解一样
     	 	// ...
     	 }
     }
    */