大家好,本周主要是在学习Rust。今天给大家讲讲Rust的生命周期。
现在,把Rust程序当成一栋管理极其严苛的实验大楼,程序中的数据就是实验室里的设备,引用是一张允许你使用某台设备的门禁卡,生命周期是这张门禁卡保证有效期的时间段,Rust编译器和借用检查就是看管这栋实验楼的安保人员。
首先不要管生命周期,来讲讲Rust到底在怕什么。
假设实验楼里有一台电脑x,我有一张可以找到并使用它的门禁卡。那么现在问题来了,如果电脑x被实验室给销毁了,但是我手里的门禁卡还在,并且依然拿着这个门禁卡想要去访问并使用这个电脑,结果就是找不到这个电脑x了。这就是Rust的悬垂引用,所以Rust的一条基本规则就是,引用绝对不能比它所指向的数据活得更久。
接下来是生命周期,首先广义上来讲,rust中的每一个值都有一个生命周期,从第一次声明到作用域结束,但是由于语义冲突,我更新换称其为存活周期;而生命周期是对引用进行标注的,生命周期一般标记为'a,或者'b,或者其他的什么名称。生命周期不是在给一个引用规定真实的寿命,而是在描述一个最低保证/关系的约束。用Rust官方的话来说就是:生命周期标注不会改变引用的实际作用域;生命周期的作用是告诉编译器多个引用之间的关系。
这里会有一个坑,就是本次调用中,多个输入引用(一个就说了,Rust生命周期的消除规则早已明示)需要能够共同保一段证有效并且足以覆盖返回值实际使用范围的一段生命周期。
拿一个例子来讲,在Rust程序中定义一个longest函数,它的作用是返回传进去的两个字符串中较长的那一个,传进去两个引用x,y并返回二者其中之一。那么在调用这个函数之前,我们没办法知道函数到底会返回x还是y,同样的编译器在编译期做生命周期检查时就更不知道了,因此需要显式指定一个生命周期'a,这个生命周期就是保证:在'a这一段时间内,引用x, y都一定是有效的。
但是如果我后续的传参中,其中一个引用来自main函数的一个代码块,而函数调用却在代码块之外,那么这就直接造成了悬垂引用,函数传参时编译器就会报错,根本轮不到生命周期标注。
那么现在问题来了,讲了这么多,生命周期标注到底是开发者在哄编译器开心,还是编译器在当S调教作为M的Rust开发者呢?
其实都不是,生命周期标注本质上是在“函数边界补充类型信息”。
之所以会产生上面两种感觉,是因为Rust在标注生命周期时的分工有些特殊:
- 编译器自己知道局部代码里谁都可以活多久
- 但跨函数边界时,它不会根据函数实现去猜返回值到底来自哪个参数。
- 所以开发者必须在函数签名里把这种引用之间的关系给说清楚。
还是之前讲过的longest函数例子,这里对生命周期的标注不是开发者在求编译器把这俩参数当成可以活一样长的;也不是编译器在说:“不写'a我就拿鞭子抽你”。
而是在签一份合同:
开发者:
这个函数可能返回 x,也可能返回 y。
所以返回值的有效期不能超过
x 和 y 都能够共同保证有效的那段时间。
编译器:
好。
那我在每次调用时,根据现场实际情况,
检查有没有这样一段生命周期。
这才是理解的最佳模型。相当于开发者只是在描述关系,编译器负责计算生命周期和检查关系是否成立。
生命周期标注会让人感觉开发者在伺候编译器的另一个原因是:编译器很多时候能够看懂局部情况,但是在函数签名这种API边界上,它要求开发者明确表达关系。
还是上面那个longest函数,如果真的不进行生命周期的标注,那么编译器会看到两个输入的生命周期不同,一个是'a一个是'b(举例而已),它不知道开发者想承诺什么:返回值是来自x还是y?或者两者均有可能?
这不是因为编译器笨,而是调用者应该只看函数签名,就能知道“我可以怎么调用这个函数,以及它返回的东西和参数之间有什么类型关系”,而不需要钻进函数体分析实现。