生命周期

生命周期( Lifetime )

下面是一个资源借用的例子:

fn main() {

    let a = 100_i32;

    {

        let x = &a;

    }  // x 作用域结束

    println!("{}", x);

}

编译时,我们会看到一个严重的错误提示:

error: unresolved name x.

错误的意思是“无法解析 x 标识符”,也就是找不到 x , 这是因为像很多编程语言一样,Rust中也存在作用域概念,当资源离开离开作用域后,资源的内存就会被释放回收,当借用/引用离开作用域后也会被销毁,所以 x 在离开自己的作用域后,无法在作用域之外访问。

上面的涉及到几个概念:

Owner: 资源的所有者 a

Borrower: 资源的借用者 x

Scope: 作用域,资源被借用/引用的有效期

强调下,无论是资源的所有者还是资源的借用/引用,都存在在一个有效的存活时间或区间,这个时间区间称为生命周期, 也可以直接以Scope作用域去理解。

所以上例子代码中的生命周期/作用域图示如下:

            {    a    {    x    }    *    }

所有者 a:        |________________________|

借用者 x:                  |____|            x = &a

  访问 x:                            |      失败:访问 x

可以看到,借用者 x 的生命周期是资源所有者 a 的生命周期的子集。但是 x 的生命周期在第一个 } 时结束并销毁,在接下来的 println! 中再次访问便会发生严重的错误。

我们来修正上面的例子:

fn main() {

    let a = 100_i32;

    {

        let x = &a;

        println!("{}", x);

    }  // x 作用域结束

}

这里我们仅仅把 println! 放到了中间的 {}, 这样就可以在 x的生命周期内正常的访问 x ,此时的Lifetime图示如下:

            {    a    {    x    *    }    }

所有者 a:        |________________________|

借用者 x:                  |_________|      x = &a

  访问 x:                        |            OK:访问 x

隐式Lifetime

我们经常会遇到参数或者返回值为引用类型的函数:

fn foo(x: &str) -> &str {

    x

}

上面函数在实际应用中并没有太多用处,foo 函数仅仅接受一个 &str 类型的参数(x为对某个string类型资源Something的借用),并返回对资源Something的一个新的借用。

实际上,上面函数包含该了隐性的生命周期命名,这是由编译器自动推导的,相当于:

fn foo<'a>(x: &'a str) -> &'a str {

    x

}

在这里,约束返回值的Lifetime必须大于或等于参数x的Lifetime。下面函数写法也是合法的:

fn foo<'a>(x: &'a str) -> &'a str {

    "hello, world!"

}

为什么呢?这是因为字符串"hello, world!"的类型是&'static str,我们知道static类型的Lifetime是整个程序的运行周期,所以她比任意传入的参数的Lifetime'a都要长,即'static >= 'a满足。

在上例中Rust可以自动推导Lifetime,所以并不需要程序员显式指定Lifetime 'a 。

'a是什么呢?它是Lifetime的标识符,这里的a也可以用b、c、d、e、...,甚至可以用this_is_a_long_name等,当然实际编程中并不建议用这种冗长的标识符,这样会严重降低程序的可读性。foo后面的<'a>为Lifetime的声明,可以声明多个,如<'a, 'b>等等。

另外,除非编译器无法自动推导出Lifetime,否则不建议显式指定Lifetime标识符,会降低程序的可读性。

显式Lifetime

当输入参数为多个借用/引用时会发生什么呢?

fn foo(x: &str, y: &str) -> &str {

    if true {

        x

    } else {

        y

    }

}

这时候再编译,就没那么幸运了:

error: missing lifetime specifier [E0106]

fn foo(x: &str, y: &str) -> &str {

                            ^~~~

编译器告诉我们,需要我们显式指定Lifetime标识符,因为这个时候,编译器无法推导出返回值的Lifetime应该是比 x长,还是比y长。虽然我们在函数中中用了 if true 确认一定可以返回x,但是要知道,编译器是在编译时候检查,而不是运行时,所以编译期间会同时检查所有的输入参数和返回值。

修复后的代码如下:

fn foo<'a>(x: &'a str, y: &'a str) -> &'a str {

    if true {

        x

    } else {

        y

    }

}

Lifetime推导

要推导Lifetime是否合法,先明确两点:

输出值(也称为返回值)依赖哪些输入值

输入值的Lifetime大于或等于输出值的Lifetime (准确来说:子集,而不是大于或等于)

Lifetime推导公式: 当输出值R依赖输入值X Y Z ...,当且仅当输出值的Lifetime为所有输入值的Lifetime交集的子集时,生命周期合法。

    Lifetime(R) ⊆ ( Lifetime(X) ∩ Lifetime(Y) ∩ Lifetime(Z) ∩ Lifetime(...) )

对于例子1:

fn foo<'a>(x: &'a str, y: &'a str) -> &'a str {

    if true {

        x

    } else {

        y

    }

}

因为返回值同时依赖输入参数x和y,所以

    Lifetime(返回值) ⊆ ( Lifetime(x) ∩ Lifetime(y) )

    即:

    'a ⊆ ('a ∩ 'a)  // 成立

定义多个Lifetime标识符

那我们继续看个更复杂的例子,定义多个Lifetime标识符:

fn foo<'a, 'b>(x: &'a str, y: &'b str) -> &'a str {

    if true {

        x

    } else {

        y

    }

}

先看下编译,又报错了:

<anon>:5:3: 5:4 error: cannot infer an appropriate lifetime for automatic coercion due to conflicting requirements [E0495]

<anon>:5        y

                ^

<anon>:1:1: 7:2 help: consider using an explicit lifetime parameter as shown: fn foo<'a>(x: &'a str, y: &'a str) -> &'a str

<anon>:1 fn bar<'a, 'b>(x: &'a str, y: &'b str) -> &'a str {

<anon>:2    if true {

<anon>:3        x

<anon>:4    } else {

<anon>:5        y

<anon>:6    }

编译器说自己无法正确地推导返回值的Lifetime,读者可能会疑问,“我们不是已经指定返回值的Lifetime为'a了吗?"。

这儿我们同样可以通过生命周期推导公式推导:

因为返回值同时依赖x和y,所以

    Lifetime(返回值) ⊆ ( Lifetime(x) ∩ Lifetime(y) )

    即:

    'a ⊆ ('a ∩ 'b)  //不成立

很显然,上面我们根本没法保证成立。

所以,这种情况下,我们可以显式地告诉编译器'b比'a长('a是'b的子集),只需要在定义Lifetime的时候, 在'b的后面加上: 'a, 意思是'b比'a长,'a是'b的子集:

fn foo<'a, 'b: 'a>(x: &'a str, y: &'b str) -> &'a str {

    if true {

        x

    } else {

        y

    }

}

这里我们根据公式继续推导:

    条件:Lifetime(x) ⊆ Lifetime(y)

    推导:Lifetime(返回值) ⊆ ( Lifetime(x) ∩ Lifetime(y) )

    即:

    条件: 'a ⊆ 'b

    推导:'a ⊆ ('a ∩ 'b) // 成立

上面是成立的,所以可以编译通过。

推导总结

通过上面的学习相信大家可以很轻松完成Lifetime的推导,总之,记住两点:

输出值依赖哪些输入值。

推导公式。

Lifetime in struct

上面我们更多讨论了函数中Lifetime的应用,在struct中Lifetime同样重要。

我们来定义一个Person结构体:

struct Person {

    age: &u8,

}

编译时我们会得到一个error:

<anon>:2:8: 2:12 error: missing lifetime specifier [E0106]

<anon>:2    age: &str,

之所以会报错,这是因为Rust要确保Person的Lifetime不会比它的age借用长,不然会出现Dangling Pointer的严重内存问题。所以我们需要为age借用声明Lifetime:

struct Person<'a> {

    age: &'a u8,

}

不需要对Person后面的<'a>感到疑惑,这里的'a并不是指Person这个struct的Lifetime,仅仅是一个泛型参数而已,struct可以有多个Lifetime参数用来约束不同的field,实际的Lifetime应该是所有fieldLifetime交集的子集。例如:

fn main() {

    let x = 20_u8;

    let stormgbs = Person {

                        age: &x,

                    };

}

这里,生命周期/Scope的示意图如下:

                  {  x    stormgbs      *    }

所有者 x:              |________________________|

所有者 stormgbs:                |_______________|  'a

借用者 stormgbs.age:            |_______________|  stormgbs.age = &x

既然<'a>作为Person的泛型参数,所以在为Person实现方法时也需要加上<'a>,不然:

impl Person {

    fn print_age(&self) {

        println!("Person.age = {}", self.age);

    }

}

报错:

<anon>:5:6: 5:12 error: wrong number of lifetime parameters: expected 1, found 0 [E0107]

<anon>:5 impl Person {

              ^~~~~~

正确的做法是

impl<'a> Person<'a> {

    fn print_age(&self) {

        println!("Person.age = {}", self.age);

    }

}

这样加上<'a>后就可以了。读者可能会疑问,为什么print_age中不需要加上'a?这是个好问题。因为print_age的输出参数为(),也就是可以不依赖任何输入参数, 所以编译器此时可以不必关心和推导Lifetime。即使是fn print_age(&self, other_age: &i32) {...}也可以编译通过。

如果Person的方法存在输出值(借用)呢?

impl<'a> Person<'a> {

    fn get_age(&self) -> &u8 {

        self.age

    }

}

get_age方法的输出值依赖一个输入值&self,这种情况下,Rust编译器可以自动推导为:

impl<'a> Person<'a> {

    fn get_age(&'a self) -> &'a u8 {

        self.age

    }

}

如果输出值(借用)依赖了多个输入值呢?

impl<'a, 'b> Person<'a> {

    fn get_max_age(&'a self, p: &'a Person) -> &'a u8 {

        if self.age > p.age {

            self.age

        } else {

            p.age

        }

    }

}

类似之前的Lifetime推导章节,当返回值(借用)依赖多个输入值时,需显示声明Lifetime。和函数Lifetime同理。

其他

无论在函数还是在struct中,甚至在enum中,Lifetime理论知识都是一样的。希望大家可以慢慢体会和吸收,做到举一反三。

总结

Rust正是通过所有权、借用以及生命周期,以高效、安全的方式近乎完美地管理了内存。没有手动管理内存的负载和安全性,也没有GC造成的程序暂停问题。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,137评论 6 511
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,824评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,465评论 0 357
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,131评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,140评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,895评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,535评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,435评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,952评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,081评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,210评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,896评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,552评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,089评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,198评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,531评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,209评论 2 357

推荐阅读更多精彩内容