玩转Rust高级应用 如何编译器对于省略掉的生命周期,不使用“自动推理”策略呢?
所以它比其他任何生命周期都长。这意味着,任意一个生命周期 'a 都满足’static:'a。
如果我们把变量t 的真实生命周期记为’t, 那么这个生命周期 t实际上是变量t从“出生”到“死亡”的区间,即从第11行到第14行。在函数被调用的时 候,它传入的实际参数是&t,它是指向t 的引用。那么可以说,在调用的时候,这个泛型 参数 a 被实例化为了t。根据函数签名,基于返回类型的生命周期与参数是一致的,可以推理出test 函数的返回类型是&'ti32。如果我们把x 的生命周期记为 x, 那么 x 代表的就是从第12行到第14行。
这条let x =text(&t);语句实际上是把&'t i32类型 的变量赋值给&'x i32类型的变量。这个赋值是否合理呢?它应该是合理的。因为这两个 生命周期的关系是’t:'x 。test返回的那个指针在 t 这个生命周期范围内都是合法的,在一个被 t包围的更小范围的生命周期内,它当然也是合法的。所以,上面这个例子可以 编译通过。
接下来,我们把上面这个例子稍作修改,让test 函数有两个生命周期参数,其中一个给 函数参数使用,另外一个给返回值使用:
fn test<'a,'b>(arg:&'a T)->&'b i32
{ &arg.member
}
编译时果然出了问题,在&arg.member 这一行,报了生命周期错误。这是为什么呢? 因为这一行代码是把&a i32类型赋值给&b i32类型。a 和 b 有什么关系?答案 是什么关系都没有。所以编译器觉得这个赋值是错误的。怎么修复呢?指定 a:b 就可以 了 。a比 b“活”得长,自然,&'a i32类型赋值给&'b i32类型是没问题的。验证如下:
fn test<'a,'b>(arg:&'a T)->&'b i32
where 'a:'b
{&arg.member
}
经过这样的改写后,我们可以认为,在test函数被调用的时候,生命周期参数 'a和 'b 被分别实例化为了’t和 'x。它们刚好满足了where条件中的 't:x 约束。而&arg. member 这条表达式的类型是&¹t i32,返回值要求的是& ¹x i32类型,可见这也是合法 的。所以test函数的生命周期检查可以通过。
上述示例是读者比较难理解的地方。以下两种写法都是可行的:
fn test<'a>(arg:&'a T)->&'a i32
fn test<'a,'b>(arg:&'a T)->&'b i32 where 'a:'b
这里的关键是,Rust 的引用类型是支持“协变”的。在编译器眼里,生命周期就是一个 区间,生命周期参数就是一个普通的泛型参数,它可以被特化为某个具体的生命周期。
我们再看一个例子。它有两个引用参数,共享同一个生命周期标记:
fn select<'a>(arg1:&'a i32,arg2:&'a i32)->&'a i32 {if *arg1 >*arg2 {arg1}else{arg2}
}
fn main(){let x =1;let y =2;let selected =select(&x,&y);println!("{}",selected);
}
上述示例中,select 这个函数引入了一个生命周期标记,两个参数以及返回值都是用 的这个生命周期标记。同时我们注意到,在调用的时候,传递的实参其实是具备不同的生命 周期的。x 的生命周期明显大于y 的生命周期,&x 可存活的范围要大于&y 可存活的范围, 我们把它们的实际生命周期分别记录为x 和y 。select函数的形式参数要求的是同样的 生命周期,而实际参数是两个不同生命周期的引用,这个类型之所以可以匹配成功,就是因 为生命周期的协变特性。编译器可以把&x 和 &y 的生命周期都缩小到某个生命周期 a 以 内,且满足x :a,y:a。返回的selected 变量具备 a 生命周期,也并没有超过 x 和 y 的范围。所以,最终的生命周期检查可以通过。
类型的生命周期标记
如果自定义类型中有成员包含生命周期参数,那么这个自定义类型也必须有生命周期参 数。示例如下:
struct Test<'a>{
member:&'a str }
在使用impl 的时候,也需要先声明再使用:
impl<'t>Test<'t>{fn test<'a>(&self,s:&'a str){}
}
impl后面的那个’七是用于声明生命周期参数的,后面的 Test<'t>是在类型中使用这 个参数。如果有必要的话,方法中还能继续引入新的泛型参数。
如果在泛型约束中有where T: 'a 之类的条件,其意思是,类型T 的所有生命周期 参数必须大于等于 'a。要特别说明的是,若是有where T:'static的约束,意思则是, 类型T 里面不包含任何指向短生命周期的借用指针,意思是要么完全不包含任何借用,要么 可以有指向’static 的借用指针。
省略生命周期标记
在某些情况下,Rust 允许我们在写函数的时候省略掉显式生命周期标记。在这种时候, 编译器会通过一定的固定规则为参数和返回值指定合适的生命周期,从而省略一些显而易见 的生命周期标记。比如我们可以写这样的代码:
fn get_str(s:&String)->&str{s.as_ref()
}
实际上,它等同于下面这样的代码,只是把显式的生命周期标记省略掉了而已:
fn get_str<'a>(S:&'a String)->&'a str{s.as_ref()
}
若把以上代码稍微修改一下,返回的指针将并不指向参数传入的数据,而是指向一个静 态常量,代码如下:
fn get_str(s:&String)->&str{println!("call fn{}",s);"hello world"
}
这时,我们期望返回的指针实际上是& 'static str 类型。测试代码如下:
fn main(){
let c =String::from("haha");
let x:&'static str =get_str(&c);
println!("{}",x);
}
可以看到,在get_str 函数中,返回的是一个指向静态字符串的指针。在主函数的调 用方,我们希望变量x 指向一个“静态变量”。可是这一次,我们发现了编译错误:
error: c does not live long enough
按照分析,变量x 理应指向一个 static 生命周期的变量,根本不是指向C 变量,它 的存活时间足够长,为什么编译器没发现这一点呢?这是因为,编译器对于省略掉的生命周 期,不是用的“自动推理”策略,而是用的几个非常简单的“固定规则”策略。
这跟类型自 动推导不一样,当我们省略变量的类型时,编译器会试图通过变量的使用方式推导出变量的 类型,这个过程叫 “type inference”。而对于省略掉的生命周期参数,编译器的处理方式就 简单粗暴得多,它完全不管函数内部的实现,并不尝试找到最合适的推理方案,只是应用几 个固定的规则而已,这些规则被称为“lifetime elision rules”。
以下就是省略的生命周期被自 动补全的规则:
- 每个带生命周期参数的输入参数,每个对应不同的生命周期参数;
- 如果只有一个输入参数带生命周期参数,那么返回值的生命周期被指定为这个参数;
- 如果有多个输入参数带生命周期参数,但其中有&self、&mut self,那么返回值 的生命周期被指定为这个参数;
- 以上都不满足,就不能自动补全返回值的生命周期参数。
这时再回头去看前面的例子,可以知道,对于这个函数:
fn get_str(s:&String)->&str{println!("call fn{}",s);"hello world"
}
编译器会自动补全生命周期参数:
fn get_str<'a>(s:&'aprintln!("call"hello world" String)->&'a fn{}",s); str{
}
所以,当我们调用
let x:&static str =get_str(&c);
这句代码的时候,就发生了编译错误。了解了这些,修复方案也就很简单了。在这种情 况下,我们不能省略生命周期参数,让编译器给我们自动补全,自己手写就对了:
fn get_str<'a>(s:&a String)->&static str{println!("call "hello world" fn{}",s);
}
或者只手写返回值的生命周期参数,输入参数靠编译器自动补全:
fn get_str(s:&String)->&static str{…}
最后, 一句话总结,elision !=inference,省略生命周期参数和类型自动推导的原理是完 全不同的。
