本文摘自《深入RUST标准库》一书,目前已经正式出版,正在全网发售。如果对本文中涉及的若干知识想进一步了解,请阅读此书。
RUST在定义一个变量时,实际上把变量在逻辑上分成了两个部分,变量的内存块与变量内容。
变量类型定义了内存块内容的格式,变量声明语句定义了一个内存块,变量初始化赋值则在内存块中写入初始化变量内容。
所有权指变量内容的独占性。所有权转移指的是变量内容在不同的内存块之间的转移(浅拷贝)。当变量内容转移到新的内存块,旧的内存块就失去了这个变量内容的所有权。由此可见,变量名实际仅代表一个内存块,内存块的变量内容与变量名是一个暂时关联关系,RUST定义这种关联关系为绑定。
设计所有权的目的是保证对变量进行清理操作的正确,如果一个变量内容在多个内存块中有效,变量清理的正确性用静态编译的方法无法保证。
这里有个例外,就是实现Copy trait的类型变量不做所有权转移操作,实现Copy trait的类型可通过栈拷贝完成变量内容赋值,清理也可以仅通过通常的调用栈返回完成。
RUST被设计成自动调用变量类型的drop以完成清理,对变量的生命周期跟踪成为一个必然的选择,在判断变量的生命周期终结的时候调用变量的drop函数。
RUST采用生命周期仅与内存块(变量名)相关联的设计,这样的设计容易对生命周期进行跟踪。没有绑定所有权的内存块在生命周期终结不做任何操作,拥有所有权的内存块生命周期终结会自动触发变量的drop操作。
如果仅仅考虑drop操作,那生命周期的方案不会太复杂,但RUST决定用生命周期同时解决另一个问题,变量引用导致的野指针问题。因为所有权的关系,RUST将变量引用改了一个RUST的名字——借用,意味着对所有权的借用。
用生命周期解决借用导致的野指针问题思路很简单,就是借用的生命周期应该短于所有权的生命周期。但这个简单的思路却需要极为复杂的设计来完成,对这个复杂设计的理解也成了RUST最被人诟病的点。
理想的生命周期方案是完全由编译器搞定,程序员不要参与。但这显然不可能,编译器没办法在所有的情况下都能够完成全部的推断,势必需要程序员在编码中给出提示。生命周期因此成为rust的一个语法部分。
首先,生命周期被设计成一种实现继承语法的类型,每一个生命周期都是一个类型,不同的生命周期之间的关系用类型继承语法来完成。生命周期类型的继承具体而言: 假设有两个生命周期类型A和B,如果A完全被B包含在内,那就说B继承于A。A是基类型,B是子类型。从继承的概念,B类型能被转换为A类型,A类型无法转换为B类型。也就是说B类型的值能赋给A类型的变量,A类型的值无法赋给B类型变量。 生命周期是类型这一点与直观感觉有区别,毕竟,一个作用域给人的感觉就应该是个值。但是,用类型这个方案:
- 可以利用类型系统来完成生命周期方案,没有给rust编译器增加太大的负担,代码也几乎不受影响。
- 利用继承语法,在变量赋值时根据类型能否转换完成生命周期长短的判断,是极为巧妙的,简化的,自然的设计。
因为生命周期针对内存块设计,而在转移所有权的操作中,是两个不同的内存块发生的联系,他们的生命周期彼此独立。所以所有权转移时,所有权变量的类型层次上不涉及生命周期类型转换。如果类型成员中有引用,则见下面的内容。 当对一个引用类型变量做赋值时,便出现了生命周期类型转换,举例分析如下:
- 当声明一个类型引用的变量时,例如:
let a: &i32
实质声明了一个i32类型引用的内存块,这个内存块有一个生命周期泛型, 假设为'a, - 假设要对此变量赋值为另一个变量的引用,例如:
let b:i32 = 4; a = &b;
&b实质是对b的内存块进行引用,该内存块的生命周期假设为'b, - 赋值实质是将一个&'b i32 类型的变量赋值给&'a i32类型变量。则必然发生类型转换关系,这时,只有当'b是'a的子类型时,即'b长于'a时,这个类型转换才能被编译器认为正确。
因为引用类型是泛型的一种,那由泛型派生的类型的赋值也就会出现类型转换的问题,例如:*const T,*mut T,Box<T> ,...
具体的类型可以参考RUST Reference。这时,需要由T的继承关系推断出派生类型的继承关系。这就是变异性Variance特性存在的意义,Variance存在三种情况
- 协变covariant,泛型是子类,派生类型也是子类。泛型是父类,派生类型也是父类
- 逆变contravariant,泛型是子类,派生类型是父类。泛型是父类,派生类型是子类
- 不变invariant, 泛型的子类还是父类都推导不出派生类型是否是子类还是父类。
复合类型之间的继承的关系可根据成员变异性得出。 因为在RUST编程中引用派生类型及其赋值操作的广泛性,所以变异性是一个重要的需要被理解的概念。完整的变异性请参考RUST Reference。
每一个函数的生命周期类型转换处理正确与否在函数内完成判断(以下为根据逻辑进行的推断,可能不准确):
- 函数作用域会有一个生命周期泛型;
- 函数的定义会定义函数参数的生命周期泛型,以及这些生命周期泛型之间的继承关系。显然,函数作用域生命周期泛型是所有输入参数生命周期泛型的基类型
- 函数的定义会定义输出的生命周期泛型,以及输出的生命周期泛型与输入参数生命周期泛型的继承关系。如果输出是一个借用或由借用派生的类型或者有借用成员的复合类型,则输出的生命周期泛型必须是某一输入生命周期泛型的基类型。
- 编译器会分析函数中的作用域,针对每个作用域生成生命周期泛型,并形成这些生命周期泛型之间的继承关系,当然,函数内所有生命周期泛型都是函数作用域生命周期泛型的基类型。
- 根据这些生命周期泛型及他们之间的继承关系,处理函数内操作时引发的生命周期泛型类型转换,并对错误的转换做出错警告。
- 如果调用了其他函数,则对调用函数的输入参数及输出之间的转换是否正确判断转移至调用函数。
如果一个复合类型内部存在引用类型成员或递归至引用类型成员,则必须明确此复合类型的生命周期泛型与成员生命周期泛型的继承关系。一般复合类型的生命周期应该是基类型。
RUST编译器做了很多工作以避免生命周期泛型在代码中出现。这部分的工作仍然在持续进行中。 举几个生命周期的例子:
impl *const T{
pub const unsafe fn as_ref<'a>(self) -> Option<&'a T> {
if self.is_null() { None } else { unsafe { Some(&*self) } }
}
}
因为*const T没有生命周期类型与之相关。所以上面这个函数必须声明一个生命周期泛型用于标注返回的生命周期,此泛型独立存在,不与其他生命周期泛型有关系。因此返回的引用变量的生命周期完全决定于调用此函数的代码定义。因为返回引用的生命周期应短于self指向的内存块的生命周期,这只能由调用此函数的代码来保证。
RUST中,对于申请的堆内存内存块,通常将其与一个位于栈内存空间的智能指针的类型变量相结合。智能指针类型变量生命周期终止时,调用drop方法释放堆内存的内存块。智能指针类型通常会提供leak函数,将堆内存的内存块与智能指针类型的关联切断。这通常是一个中间状态,需要尽快再将堆内存与另一个智能指针类型的变量建立联系,以便其能重新被纳入生命周期的体系中
评论区
写评论还没有评论