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"
的引用赋给了 Foo
的 name
成员变量,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 的报错,而是想当然地认为这可能跟 a
和 b
的生命周期不一致有关。现在,可以给生命周期翻案了。
上述代码的错误之处在于,a
被 b
可变借用了,而且是可变借用,而在随后的 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);
}
小结
不再太害怕生命周期标记了。