1. 生命周期
在任何语言里,栈上的值都有自己的生命周期,它和帧的生命周期一致,而 Rust,进一步明确这个概念,并且为堆上的内存也引入了生命周期。
- 一般来说,堆内存的生命周期,会默认和其栈内存的生命周期绑定在一起。
- 默认情况下,在每个函数的作用域中,编译器就可以对比值和其引用的生命周期,来确保"引用的生命周期不超出值的生命周期"。
值的生命周期
静态生命周期
如果一个值的生命周期贯穿整个进程的生命周期 ,那么我们就称这种生命周期为静态生命周期。
当值拥有静态生命周期,其引用也具有静态生命周期。比如: &'static str 代表这是一个具有静态生命周期的字符串 引用。
一般来说,全局变量、静态变量、字符串字面量(string literal)等,都拥有静态生命周 期。我们上文中提到的堆内存,如果使用了 Box::leak 后,也具有静态生命周期。
动态生命周期
如果一个值是在某个作用域中定义的 ,也就是说它被创建在栈上或者堆上,那么其生命周期是动态的。
当这个值的作用域结束时,值的生命周期也随之结束。对于动态生命周期,我们约定用 'a 、'b 或者 'hello 这样的小写字符或者字符串来表述。 ' 后面具体是什么名字不重要, 它代表某一段动态的生命周期,其中, &'a str 和 &'b str 表示这两个字符串引用的生 命周期可能不一致。
当出现了多个参数,它们的生命周期可能不一致时,返回值的生命周期就不好确定了。编译器在编译某个函数时,并不知道这个函数将来有谁调用、怎么调用,所以,函数本身携带的信息,就是编译器在编译时使用的全部信息。
此时,就需要我们在函数签名中提供生命周期的信息,也就是生命周期标注(lifetime specifier)。在生命周期标注时,使用的参数叫生命周期参数(lifetime parameter)。 通过生命周期标注,我们告诉编译器这些引用间生命周期的约束。
生命周期参数的描述方式和泛型参数一致,不过只使用小写字母。这里,两个入参 s1、 s2,以及返回值都用 'a 来约束。生命周期参数,描述的是参数和参数之间、参数和返回 值之间的关系,并不改变原有的生命周期。
总结
- 所有引用类型的参数都有独立的生命周期 'a 、'b 等。
- 如果只有一个引用型输入,它的生命周期会赋给所有输出。
- 如果有多个引用类型的参数,其中一个是 self,那么它的生命周期会赋给所有输出。
- 使用数据结构时,数据结构自身的生命周期,需要小于等于其内部字段的所有引用的生命周期。
2. 内存管理
Rust 的创造者们,重新审视了堆内存的生命周期,发现大部分堆内存的需求在于动态大 小,小部分需求是更长的生命周期。
值的创建
struct
Rust 在内存中排布数据时,会根据每个域的对齐(aligment)对数据进行重排,使其内存大小和访问效率最好。
如果结构体的定义考虑地不够周全,会为了对齐浪费很多空间。
enum
enum是一个标签联合体(tagged union),它的大小是标 签的大小,加上最大类型的长度。定义 enum 数据结构时,有 Option 和 Result<T, E> 两种设计举例。Rust 编译器会对 enum 做一些额外的优化,让某些常用结构的内存布局更紧凑。引用类型的第一个域是个指针,而指针是不可能等于 0 的,但是我们可以复用这个指针:当其为 0 时,表示 None,否则是 Some,减少了内存占用。
vec 和 String
String 结构内部就是一个 Vec;Vec 结构是 3 个 word 的胖指针,包含:一个指向堆内存的指针 pointer、分配的堆内存的容量 capacity,以及数据在堆内存的长度 length。
很多动态大小的数据结构,在创建时都有类似的内存布局:栈内存放的胖指针,指向堆内 存分配出来的数据。
值的使用
一个值如果没有实现 Copy,在赋值、传 参以及函数返回时会被 Move。其实 Copy 和 Move 在内部实现上,都是浅层的按位做内存复制,只不过 Copy 允许你访 问之前的变量,而 Move 不允许。
内存复制是个很重的操作,效率很低。但是,如果你要复制的只是原生类型(Copy)或者栈上的胖指针(Move),不涉及堆内 存的复制也就是深拷贝(deep copy),那这个效率是非常高的,我们不必担心每次赋值 或者每次传参带来的性能损失。所以,无论是 Copy 还是 Move,它的效率都是非常高的。
在使用值的过程中,除了 Move,你还需要注意值的动态增长。因为 Rust 下,集合类型的数据结构,例如:Vec,都会在使用过程中自动扩展。但只使用了很小部分的容量,内存的使用效率很低,所以你要考虑使用,比如 shrink_to_fit 方法,来节约对内存的使用。
值的销毁
当一个值要被释 放,它的 Drop trait 会被调用。如果要释放的值是一个复杂的数据结构,比如一个结构体,那么这个结构体在调用 drop() 时,会依次调用每一个域的 drop() 函数,如果域又是一个复杂的结构或者集合类型,就会 递归下去,直到每一个域都释放干净。整个释放顺序从内到外是:先释放 HashMap 下的 key,然后释放 HashMap 堆上的表结构,最后释放栈上的内存。
3. 类型系统
类型系统其实就是,对类型进行定义、检查和处理的系统。
生命周期标注也是泛型的一部分,一个生命周期 'a 代表任意 的生命周期,和 T 代表任意类型是一样的。