向量
数组的优势也恰恰是其局限所在:其大小必须在编译时预先确定。如果你尝试创建一个大小仅在运行时才知道的数组,将会遇到编译错误:
#![allow(unused)] fn main() { let n = 10; let numbers: [u32; n]; }
error[E0435]: 尝试在常量中使用非常量值
--> src/main.rs:3:20
|
2 | let n = 10;
3 | let numbers: [u32; n];
| ^ 非常量值
对于票务管理系统来说,数组并不适用——我们在编译时不知道需要存储多少张票。这时候,Vec
就派上用场了。
Vec
Vec
是标准库提供的一个可增长的数组类型。
你可以使用Vec::new
函数创建一个空的向量:
#![allow(unused)] fn main() { let mut numbers: Vec<u32> = Vec::new(); }
然后,你可以使用push
方法向向量中添加元素:
#![allow(unused)] fn main() { numbers.push(1); numbers.push(2); numbers.push(3); }
新值会被追加到向量的末尾。
如果在创建时就知道元素值,也可以使用vec!
宏来创建一个初始化的向量:
#![allow(unused)] fn main() { let numbers = vec![1, 2, 3]; }
访问元素
访问元素的语法与数组相同:
#![allow(unused)] fn main() { let numbers = vec![1, 2, 3]; let first = numbers[0]; let second = numbers[1]; let third = numbers[2]; }
索引必须是usize
类型。
同样,你也可以使用get
方法,它返回一个Option<&T>
:
#![allow(unused)] fn main() { let numbers = vec![1, 2, 3]; assert_eq!(numbers.get(0), Some(&1)); // 如果尝试访问越界索引,会得到 `None` 而不是恐慌。 assert_eq!(numbers.get(3), None); }
访问同样进行了边界检查,复杂度为O(1)。
内存布局
Vec
是一个堆分配的数据结构。
当你创建一个Vec
时,它会在堆上分配内存来存储元素。
如果运行以下代码:
#![allow(unused)] fn main() { let mut numbers = Vec::with_capacity(3); numbers.push(1); numbers.push(2); }
得到的内存布局如下:
+---------+--------+----------+
栈 | 指针 | 长度 | 容量 |
| | | 2 | 3 |
+--|------+--------+----------+
|
|
v
+---+---+---+
堆: | 1 | 2 | ? |
+---+---+---+
Vec
跟踪三件事:
- 指针到你预留的堆区域。
- 长度,即向量中有多少个元素。
- 容量,即堆上预留空间能容纳的元素数量。
这个布局应该很眼熟:它和String
完全一样!这不是巧合:String
本质上在内部定义为字节的向量,即Vec<u8>
:
#![allow(unused)] fn main() { pub struct String { vec: Vec<u8>, } }