与 Rust 勾心斗角 · 终于要标注生命周期了

rskynet 只是一个有着 300 余行代码的小项目……或者极其渺小项目,所实现的功能是读取记载多面体信息的 OFF 文件,计算多面体的包围球,基于包围球的中心半自动化的生成 POV Ray 场景中的模型和视图文件并交由 povray 解析,为多面体生成指定视角的渲染结果。不过,这些功能几乎与本文无关,有关的仅仅是在实现这些功能的过程中,在为 Mesh 结构体定义一个方法时,遇到了需要显式标注生命周期的情况。

问题要简单化

假设有一个结构体

struct Foo {
    name: &str,
    value: i32,
}

若使用以下代码构造 Foo 的实例

let x = Foo {name: "foo", value: 1};

rustc 会无奈地指出

error[E0106]: missing lifetime specifier
  ... ... ...
  |
  |     name: &str,
  |           ^ expected named lifetime parameter

还会给出建议

elp: consider introducing a named lifetime parameter
  |
  ~ struct Foo<'a> {
  ~     name: &'a str,
  |

按照上述建议,将 Foo 的定义修改为

struct Foo<'a> {
    name: &'a str,
    value: i32,
}

问题便得到了解决,至此我见证了 Rust 的伟大发明——生命周期标注,但是究竟发生了什么?

生命周期是泛型类型

在修改后的 Foo 里,'a 出现在我熟悉的泛型参数 T 出现的位置,它是泛型参数吗?我猜,是的。万物不同,但时间都是相同的。在 Rust 语言里,每个变量都有生命周期。使用过期的变量,会导致代码被 rustc 拒绝通过。理想是好的,但 rustc 有时无法判断一个变量是否过期。例如

let x = Foo {name: "foo", value: 1};

将字符串 "foo" 的引用赋给了 Fooname 成员变量,rustc 无法确定在 x 的生命周期内,字符串 "foo" 依然健在。在我看来,"foo" 是直接编码在程序的可执行文件里的,它与程序同寿,完全没有可能在 x 的生命周期内被释放,但是 在 rustc 看来,它只知道这是个字符串的引用,而只要是引用,就可能存在引用过期变量的危险,因此它需要我明确告诉它,"foo" 能活多久。

生命周期标记

要告诉 rustc,一个变量的生命周期是多久,基本是不可能的,但是能够通过生命周期标记告诉 rustc,一个变量的生命周期至少与另一个变量相等。例如

struct Foo<'a> {
    name: &'a str,
    value: i32,
}

能够告诉 rustc,name 引用的变量至少能活得跟 Foo 的实例一样久,事实上,的确如此。

生命周期标记仅仅是变量引用的时效性给予约束,它并不能改变变量的生命周期。下面的代码可以说明这一点,

let mut x = Foo {name: "", value: 1};
{
    let a = String::from("foo");
    foo.name = a.as_str();
}
println!("({}, {})", foo.name, foo.value);

a{ ... } 构成的局部作用区域内存活,出了该区域,便会被释放。尽管 a 的部分内容(字符串切片)被 foo.name 引用,而且生命周期标记要求该部分内容的生命周期至少与 foo 相等,但实际情况并非如此,因此 rustc 会拒绝这段代码通过编译,并给出以下错误信息

error[E0597]: `a` does not live long enough
   ... ... ...
   |
   |         foo.name = a.as_str();
   |                    ^^^^^^^^^^ borrowed value does not live long enough
   |     }
   |     - `a` dropped here while still borrowed
   |     println!("({}, {})", foo.name, foo.value);
   |                          -------- borrow later used here

陈年冤案

去年的上个月在写 rhamal.pdf,在 2.8 节,我遇到了一个灵异事件,即以下代码无法通过编译:

struct Point {x: f64, y: f64, z: f64}

fn main() {
    let mut a = Point {x: 1.0, y: 2.0, z: 3.0};
    let b = &mut a;
    println!("({}, {}, {})", a.x, a.y, a.z);
    println!("({}, {}, {})", b.x, b.y, b.z);
}

当时,由于对 Rust 语法过于畏惧,以致没有耐心观察 rustc 的报错,而是想当然地认为这可能跟 ab 的生命周期不一致有关。现在,可以给生命周期翻案了。

上述代码的错误之处在于,ab 可变借用了,而且是可变借用,而在随后的 println! 语句中,a 是以不可变借用的形式出现——println! 是一个宏,其参数会自动被转化为不可变借用。在 Rust 语法里,一个变量在被当成可变借用之后,倘若对其进行不可变借用,那么就再也不能通过可变引用访问它了,反之,若对一个变量先进行不可变借用,然后再进行可变借用,这是允许的——大概可以避免数据竞争。因此,上述代码需要修改为

struct Point {x: f64, y: f64, z: f64}

fn main() {
    let mut a = Point {x: 1.0, y: 2.0, z: 3.0};
    let b = &mut a;
    println!("({}, {}, {})", b.x, b.y, b.z);
    println!("({}, {}, {})", a.x, a.y, a.z);
}

小结

不再太害怕生命周期标记了。

你可能感兴趣的:(rust)