Rust 生命周期 - lifetime in fn
出自: www.zhihu.com/column/rust-shen
生命周期 ? 什么玩意儿?我搞这么多年编程,怎么没听说过?Rust真是事妈,我恨它!
稍安勿躁。来,绑起来,打一针,且听我《维为道来》。
其实,这要上升到哲学高度。少装B,简单点。好,你想过死吗?你是不是经常,以一万年为单位,来规划自己的生活?任何事情都有生生灭灭的过程,只是你没在意。
象golang, java,python,这样的语言中,GC替你做了一切。它管理变量的生命周期。到了某个节点,就象大清算来了一样,GC收回了必要的内存。在它忙活这件事儿的时侯,整个”世界”似乎都暂停了。所以,这正是GC程序让人讨厌的地方,会卡住一下下。
象C就牛了,它才不管你这事,你自己管理内存的使用和销毁。错了它也不知道,你也蒙圈。
Rust使用生命周期的概念,以原语的方式,让我们明确定义和使用lifetime。
轻松够了吧,来个第一印象:
lifetime 是放在尖括号里的。Rust 的 lifetime 可以当做泛型系统的一部分, 或叫它泛型生命周期参数:
foo<'a, 'b> // 含义:foo的生命周期不能超出 'a 和 'b 中任一个的周期。
fn f<'a, 'b>(v1: &'a T1, v2: &'b T2) -> &'a T3 {...}
这里面的 ‘a ‘b 是泛型参数,在这里的意思就是一种约束,传入两个指针 v1 v2,返回一个指针。v1 的 lifetime 和 v2 不同,但是和最终返回的指针相同。
使用生命周期,很大的原因,是我们关注这样的问题:
实体A持有一个指向实体B的引用,则A能够访问期间B必须存活;
为什么你的C语言经常莫名其妙的挂掉,很可能就是悬空指针:人活着,钱没了。
还是以例子形式展开:
//正确: 一个参数,编译器能推断生命周期,不用注明
fn longest1(x: &str) -> &str {
x
}
//错误: 二个参数,这让编译器犯迷糊。即使没使用,也不行,编译不过去
fn longest2(x: &str,y:&str) -> &str {
print!("{}",y);
x
}
longest2错误提示:expected named lifetime parameter(渴望 命名 生命周期 参数)。
它需要你 显式的注明 生命周期,正确的方式是这样的:
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
来个测试:
#[test]
fn test_longest(){
let out =longest("abc","qwert");
println!("---> {} <---",out);
}
没问题。这样做标记有什么用呢? 标记后,生命周期会更长一些吗?
不会。只是编译器要求,生命周期必须有明确的定义,它不清楚时,你得告诉它。
如果有多个参数,你就得告诉编译器返回的引用是跟着哪个参数的生命周期。
为了引入下一个知识点,我设计一种情景:
fn foo() {
let x = 520; //本来我希望caller调用这个520
{
let x = 1314; //不小心,我在一个子域中 多写了这么一句
let out = caller(x);
println!("---> {} <---", out); //此时我们引入了一个错误,但编译器并没有发现
}
}
如何避免上例中的错误出现?那就是现在介绍的,这个东西:
fn foo<'a: 'b, 'b> // <- 'a: 'b 读作 'a 生命周期至少和 'b 一样长。
我不知道怎么称呼它,有的资料上说,这是强制转换。我感觉有点bullshit。变量的生命周期是由它所在的位置决定的,设计的能随意改动,不合情理。这个标记,应该是一种对外部参数的一种要求和限制。就象说,我要求穿泳装才能进这个门,而不是你如果穿长裙,我会给你脱掉,换成泳装。
上个例子吧:
fn choose_first<'a, 'b:'a>(first: &'a i32, second: &'b i32) -> &'a i32 {
println!("---> second={}",second);
first
}
#[test]
fn test_force() {
let var_a = 23; // 较长的生命周期
let var_b = 520; // 注意这个变量,它并不是choose_first的参数
let out:& i32 ;
{
let var_b = 1314; //var_b定义在这里会出错。
//因为choose_first 的定义中 有 'b:'a, 意思就是要求b和a的生命一样长。
out = choose_first(&var_a, &var_b);
println!("1---> var_b = {}", var_b);
};
println!("---> {} is the first", out);
println!("2---> var_b = {}", var_b);
}
因为我们在定义choose_first中作了限制,choose_first<’a,’b:’a>要求’b变量至少要和’a变量活的一样久。所以,假如写了let var_b =1314; 编代码的眼瞎,编译器可不会放过。
我们来设计一个反证例子:
fn choose_first<'a:'b, 'b>(first: &'a i32, second: &'b i32) -> &'a i32 {
println!("---> second={}",second);
first
}
#[test]
fn test_force_bad(){
let var_b=12;
let out;
{
let var_a=34;
out =choose_first(&var_a,&var_b); //编译不通过,提示 &var_a 有问题
}
println!("---> {} <---",out);
}
在这个例子中, var_a的寿命较短,出界后,out就死掉了。即便我们加了 ‘a: ‘b 也是一点毛用没有。 这个例子充分证明了我上面的说法是正确的。 在线把玩
进一步,想多一点:
看上去,这种手工标记生命周期的方式,就象手动档汽车 一样。
难道不能智能推断吗?选择最短的生命周期嘛,编译器应该能算出来。
可能是考虑到编译耗时,以及检查不一定有那么智能。
而且在函数相互递归的情况下,编译并不能推断真正的返回结果,仍然需要annotation。
所以不如手工标记。而且我感觉这个负担也不大。
反而有个好处,让写代码的,更加清醒的看清楚自己的代码逻辑,以及危险的边界。
还有,象我们刚才提到的 ‘a: ‘b 这种良好的要求和限制,你不手工指定,编译器怎么会知道。
所谓当局者迷,我看旁观者也未必清,很可能是瞎比哄哄。
看看别人怎么说:
所以如果函数有递归调用可能就没法推导了。
虽然不能全部推导,但是部分推导还是能实现的。
rust没有这么做可能是担心效率问题吧,
很多C++代码静态分析工具做了类似的事情,那用起来真的很慢。
不只函数,生命周期标注,也会出现在struct 、trait、impl里面。不说了头大。
本作品采用《CC 协议》,转载必须注明作者和本文链接