【本文正在参加金石计划附加挑战赛—第三期命题】
结构体中的类型为引用的字段的生命周期和结构体实例生命周期之间的关系一直以来都是让人难以琢磨的一件事。今天通过一两个小例子,让大家学会如何理清他们之间关系,特别是在面对层层嵌套的结构体情况,应该如何设计才能确保拿到想要的效果。
来看一看 Rust 是如何处理数据结构体中出现引用类型呢?让我们从无法通过编译的错误代码走起,下面的代码有什么问题呢? 在 MyStruct 结构体中的 r 字段的类型 &i32 也就是 i32 的引用类型。
// 下面代码无法通过编译
struct MyStruct {
r: &i32
}
let s;
{
let x = 10;
s = MyStruct { r: &x };
}
assert_eq!(*s.r, 10); // 问题是 `x` 的值以及在 {} 作用域结束处释放了
这个问题比较经典,稍微有点经验,一眼就能看出。就是在结构体中的 r 字段引用了 x 的值 ,也就是在 s 中通过 r 引用了 x 变量的值,当离开 { 形成的作用域,变量 x 的值已经被释放了,当再次去使用 s 也就导致了上面的错误。
要解决这个问题并不难,方法也很简单就是延长了引用 r 的生命周期,在下面代码通过在 & 添加 'static 让 r 所引用的 i32 值一直存在,直到程序结束。
struct MyStruct {
r: &'static i32
}
fn main(){
let s: MyStruct;
{
static X:i32 = 10;
s = MyStruct { r: &X };
}
assert_eq!(*s.r, 10);
}
这样一来 r 只能引用在程序生命周期中一直存在的 i32 值,看似不是很合理。所以可以尝试 r 类型一个生命周期参数 'a 来定义其生命周期,让其生命周期和 MyStruct 的实例的生命周期保持一致,这样也可以避免之前的问题。
struct MyStruct<'a> {
r: &'a i32
}
现在 MyStruct 类型拥有了生命周期,这里大家会有疑问,就是生命周期设计不是为了引用吗?对于,引用的生命周期也就是要和结构体进行对其
每一个在类型 S 中的值的生命周期都用 'a 来标识,'a 的生命周期应该比其所在 S 更长久。
回到之前代码 s = MyStruct { r: &x }; 创建了一个 MyStruct 类型值,并且给值定义了一个生命周期 'a。当你将 &x 作为 r 字段的值,也就是限制了 'a 生命周期需要给 x 的生命周期对齐。
s = MyStruct { ... } 是将初始化了 MyStruct 结构体,然后用变量 s 来接收,该变量的生命周期一直延续到这个 example 结束,而不再是在跳出作用域(一对花括号)就结束,这是因为在指定了结构体的 'a 的生命周期,要长于 s 的生命周期保持一致。从而避免出现之前的问题。
struct C;
struct B<'b> {
c: &'b C,
}
struct A<'a> {
b: B<'a>,
c: C,
}
fn main() {
let c1 = C;
let _ = A {
c: c1,
b: B { c: &c1 },
};
}
简单分析一下上面的代码当 c1 赋值给了 c 是发生了所有权的转移,所以编译是无法通过的。解决方案也很简单就是让 struct C 实现 Copy 和 Clone 的 trait 就行。
#[derive(Copy, Clone)]
struct C;
不过这样做似乎并不是最好的做法,我们还是想保持 C 结构体不支持 copy 和 clone。
struct C;
struct B<'b> {
c: &'b C,
}
struct A<'a> {
b: B<'a>,
c: &'a C, // 现在将其也修改为引用
}
fn main() {
let c1 = C;
let _ = A {
c: &c1,
b: B { c: &c1 },
};
}
在上面的代码中,我们将 A 中 c 字段接收值也是 C 的引用的类型,并且通过生命周期标识来保持 A 中 C 引用的生命周期和 B 中的对 C 引用的生命周期保持一致。
进一步展开,如果我们想要实现构造函数通过 new fn 来完成对 A 类型实例化。这个代码初一看似乎没啥问题。
impl<'a> A<'a> {
fn new() -> A<'a> {
let c1 = C;
A {
c: &c1,
b: B { c: &c1 },
}
}
}
尝试编译一下上面的代码,结果无法通过编译。这又是什么原因呢? 好为了更清楚地进行分析,先将其中fn new() 提取出来进行分析。
fn new() -> A<'a> {
let c1 = C; // 在此创建 c1
A {
c: &c1, // ...获取 c1 的引用
b: B { c: &c1 }, // ...获取 c1 的引用
}
} // 离开这个作用域时 c1 已经销毁了
很显然,函数执行结束,会返回 A 结构体的实例,同时在函数 fn new 的作用域内创建的变量 c1 也就被销毁了,所以想要修改上面的问题,就需要将 c1 从函数内部移到函数外部,作为一个引用传入,这样就可以解决上面的问题,代码如下。
struct C;
struct B<'b> {
c: &'b C,
}
struct A<'a> {
b: B<'a>,
c: &'a C
}
fn main() {
let c1 = C;
let _ = A::new(&c1);
}
impl<'a> A<'a> {
fn new(c: &'a C) -> A<'a> {
A {c: c, b: B{c: c}}
}
}