Rust学习笔记/寿元

renyuneyun 2018年08月06日(周一) 1 mins

寿元是Rust因对堆栈的抽象及保证内存安全而生的(所有权外的)另一概念。

基本而言,寿元代表的是值的有效范围。许多语言(如C++)中的作用域即是寿元的一个方面,但Rust中的寿元是一更通用/抽象且明确要求的概念。

我见到不少中文Rust相关材料中将lifetime翻译为「生命周期」,实在是令人瞠目结舌:「生命周期」是对lifecycle的翻译,而这些人似乎完全没有看到Rust中这个概念是lifetime

当然,完全存在另一种可能:他们看到了,仍然将其翻译为「生命周期」。这样则是体现了当代中文翻译中的一大问题:墨守成规和懒惰——固守已有的「词」,宁可用错也不愿意另外造词。当然这也是环境造就的,毕竟中文本以字为基础,但一则现在的教育让人们习惯以词(字组)为基础,二则现在通行的汉字数字化方案对新造汉字很不友好

「寿元」未必是最优的翻译,但却是我现下能想到的最信达雅的翻译。如果有达成共识的不更差的翻译,我个人很愿意迁移。

Rust中,对寿元的需求存在于引用(借用)之上——只有引用的值才会有值的寿元不同于变量寿元的情况,而所拥有的值寿元永远和变量一致。理论上来说,每一引用均包含寿元,且每个引用的寿元均会被检查;但出于方便考虑,部分情况下寿元不需要显式标明(见后文)。

由于行文顺序,此章节不讨论自定义数据类型(主要是结构体)上寿元的使用,而是将其放在后面相应章节(虽然其本质即为本章节所讨论内容)。

寿元与所有权

所有权一节介绍过,值的所有权仅归属于一个变量,其他变量只能对其进行借用(或复制产生新值)。Rust规定的寿元和其所归属变量的寿元是统一的。因而,如下代码不合法:

{
    let r;

    {
        let x = 5;
        r = &x;
    }

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

其原因就是x的值(5)的寿元和其所归属的变量(x)的寿元一样长,离开作用域(代码块)后就失效。Rust编译器的报错很明确地说明该问题:

error[E0597]: `x` does not live long enough
  --> src/main.rs:7:5
   |
6  |         r = &x;
   |              - borrow occurs here
7  |     }
   |     ^ `x` dropped here while still borrowed
...
10 | }
   | - borrowed value needs to live until here

可以看到,将寿元概念明确化可以让原本一些运行时的错误得以在编译期检查出来。当然,寿元的概念并不仅于此,这只是其最简单的例子。

使用寿元最多的地方在函数签名中。在介绍使用之前,首先介绍寿元的语法及语义。

寿元语法及语义

寿元仅用在引用之上,所以仅当变量是引用类型时才可能需要声明寿元。寿元的标识是单引号(')加正常关键字名,通常仅用小写字母且非常短;寿元标识放在引用符号之后,且和类型之间用空格分隔开来。

&i32        // 引用
&'a i32     // 具有显式寿元的引用
&'a mut i32 // 具有显式寿元的可变引用

需要注意的是,寿元声明不影响值的寿元,而仅仅是标明其寿元(以帮助编译器和编程人员)——值的实际使用是唯一影响其实际寿元的途径/原因。因而,如果标明的寿元和实际寿元有冲突,编译器会直接报错。

如前文所说,寿元是基于「借用」这一概念的,所以考虑寿元时可以用类似的方法,也可以从堆栈的角度考虑(但寿元实际上带来了更细粒度的控制)。

多数情况下,如果是在同一代码块内对变量进行借用,寿元往往不需要显式声明。而进行函数定义时,由于有通用性考虑,往往需要显式声明寿元。

函数签名

显式的寿元使用最广泛的两处之一便是函数签名部分(另一处是自定义数据类型,尤结构体)。

其形式为:

fn max<'a>(num1: &'a i32, num2: &'a i32) -> &'a i32 {
    if num1 > num2 {
        num1
    } else {
        num2
    }
}

fn main() {
   let a = 3;
   let b = 5;
   let c = max(&a, &b);
   print!("{}", c);
}

在函数名之后参数表之前,放置一对尖括号,其内声明会用到的寿元(用逗号分隔),然后在参数类型和返回值类型上使用。该语法同时也是Rust中指明泛型类型参数的语法(寿元和类型参数放在一起)。

本例中,该函数直接返回两参数中的一个,所以返回值的寿元和参数的寿元一致(更严格地说:返回值所借用的值,参数所借用的值)。在实际使用中要自己考虑清楚具体的寿元,并且一般应以最小标注为标准。

为简便起见,在符合规则的情况下可以不指明部分参数的寿元(编译器会自动进行尝试)。但该「规则」并不具有理论上的统一性,而仅是因为他们很「常见」(所以可能还会增加)。官方教程的这个部分对此进行了一点讨论。

可以看到,寿元机制使得编译器可以检查函数返回的值(在返回后)是否仍然有效,并且会持续追踪。这使得悬空指针的问题被彻底避免掉,而且是在编译期即可检查出来(且不要忘记Rust的编译速度很快)。


您可以在Hypothesis上的該群組內進行評論,或使用下面的Disqus評論。