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 协议》,转载必须注明作者和本文链接
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!