std::vec::Vec 的基本法
- 2019-07-15
- Phosphorus15
    众所周知,std::vec::Vec是Rust中最常用的一种变长数组实现。由于实现了诸多特性,并且可以被方便地解构成Slice,在生产环境中被广泛应用。
    和Rust所保证的一致,在常规情况下使用Vec的实现,是不会(或者说极少会)导致内存泄露等问题的。但同时,Vec本身的实现也提供了一些非安全(unsafe)的方法,在此假设读者或多或少已经接触过这些非安全方法,在此基础上对Vec进行介绍。
来自Vec的基本法
    很多使用者倾向于把Vec当成黑箱,或者仅仅了解其内存的分配机制,实际上,能够在不了解这些的情况下安全地使用Vec正是达到了其设计目的。但当对Vec的非安全方法有需求时,明白其内部实现就至关重要。实际上,Rust在官方文档中对其实现做了一系列的保证(Guarantees),以防止开发者在非安全情况下使用Vec时踩雷。
Vec的结构
    Rust对向量结构体的成员结构做了保证——在任意情况下,Vec均由一个三元组(Triplet)组成。这个三元组分别包含了向量的长度(length)——指示目前向量内的有效元素数,容量(capacity)——指示向量目前已分配的总空间,与指向数据的指针(pointer)。并且有以下两点:
- 指针(pointer)永远是非空的(空指针优化)
- 不保证三元组变量的顺序
    在Rust 1.34.0的标准库中,Vec的定义如下:
| 1 | 
 | 
    其中,Global是一个默认的全局内存分配实现,其实际大小为0。在RawVec中不占任何空间。在上古版本aplha 1.0中,有一个更加直观的简化定义
| 1 | pub struct Vec<T> { | 
    值得注意的是,两个版本虽然相近,但是len和cap的顺序却被调换了。
    Unique<T>也是一个很有意思的类型,在此可以暂时看作非空指针。
空间分配与管理
 简单来说,向量的空间分配主要存在于两种情况——创建时和扩张时
    在创建一个向量时,任何显式或隐式地指定一个空向量的行为会致使空间不被预先分配,此时ptr : Unique<T>为空(即Unique::empty())。这些行为包括但不限于:
- 使用 - Vec::new()初始化
- 使用 - vec![]宏
- 调用 - Vec::with_capacity(0)
- 用大小为 - 0的类型作为- Vec的泛型参数- 按照 - Rust文档的定义,在创建时分配内存的前提是有- mem::size_of::<T>() * capacity() > 0- 以下是一个段证明代码 
| 1 | pub struct Empty(); // 空类型 | 
 获得的输出如下:
| 1 | using vec![] | 
 从输出中,可以看出:
- 只有以非空的形式初始化向量,才会使向量分配有效内存,否则Vec中的指针为空
- 以空类型创建向量并不会导致任何空间分配的行为
- 以空类型创建的向量对应的cap是一个特殊值,对应的是usize::MAX(即2^64 - 1)
    除了在创建时分配内存,向量扩张时,如果出现已分配内存不足的情况,将会触发空间分配。这种分配通常会导致内存拷贝的发生。例如,Vec::push的实现有如下判断
| 1 | pub fn push(&mut self, value: T) { | 
 这意味着,当对向量元素进行追加时,如果分配的内存已经不足,向量将会自动扩张。
    Rust并未显式地保证任何扩张策略,但在目前的实现中,只有在临界条件下才会触发扩张,而且扩张的空间则是刚好够新增的元素使用。因此,在实际生产环境中,为了减少内存拷贝的触发次数,推荐预先指定好向量的容量,或者在适当时刻调用reserve以及reserve_exact来预分配内存。
内存的使用原则
- 在默认情况下, - Vec会保证尽量最小的内存使用量- 1 
 2
 3
 4
 5
 6
 7- fn main() { 
 let mut v = vec![0];
 let v2 = vec![0, 2, 3, 7];
 v.extend(v2.iter());
 println!("len of v: {}", v.len());
 println!("capacity of v : {}", v.capacity());
 }- 输出: - 1 
 2- len of v: 5 
 capacity of v : 5
- Vec不会主动覆写任何已被移出向量元素原空间的内容,但也不保证这些空间不被用作其它用途- 1 
 2
 3
 4
 5
 6
 7
 8
 9
 10- fn main() { 
 let mut v = vec![2333, 3333, 6666];
 let pop_v3 = v.pop();
 let ptr = v.as_ptr();
 let v3 = unsafe {
 std::ptr::read(ptr.add(2))
 };
 println!("{}", pop_v3.expect("should not panic"));
 println!("{}", v3);
 }- 输出: - 1 
 2- 6666 
 6666
- 在保证写入内容有效的情况下,可以通过 - unsafe的方式在长度以外的范围写入数据,并手动将长度调整至此位置- 1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11- fn main() { 
 let mut v: Vec<i32> = Vec::with_capacity(3);
 unsafe {
 let ptr = v.as_mut_ptr();
 std::ptr::write(ptr, 5700);
 std::ptr::write(ptr.add(1), 2600);
 std::ptr::write(ptr.add(2), 3900);
 v.set_len(3);
 }
 println!("{:?}", v)
 }- 输出: - 1 - [5700, 2600, 3900] 
- 不论何时, - Vec所分配的内存都不会被优化到栈上,而是统一从堆上分配。(有一个- crate实现了这种优化,可以在GitHub上看到)
- 不保证向量中元素被 - drop的顺序。