Array/Slice/Vector in Rust
不管哪种编程语言,最常用的数据类型不外乎数值、字符串以及数组。这里数组是一种泛称,一般指能存放多个元素的集合,当然这里的集合也不是严格的数学定义。
Array
先来看数组。
一个 array 是一组相同类型的数据集合,这些数据位于连续的内存块中,且保存在栈上而不是堆上。
以下都是数组的基本使用方式。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
主要有 3 种方法创建数组:
[V]
:直接使用元素值,不指定数组类型,比如let a = [1, 2, 3, 4]
[T; N]
:声明一个类型为 T 长度为 N 的数组,比如let a: [i32; 4]
[V; N]
:声明一个每个元素值为 V,长度为 N 的数组,比如let a: [0; 4]
。当然数组的类型为 V 的类型 T。
如果我们声明中使用了可变关键字 mut ,则表示该数组的元素值可以被修改:
1 2 |
|
Rust 中数组最大的特点是大小不可变,这对于一些编程语言用户来说能难以理解,也就是说虽然我们能修改数组中某个元素的值,但是不能往里添加元素,或者删除元素。
总结一下就是数组在编译时分配大小,保存在栈上,数组长度不可变,虽然我们能改变数组中某个元素的值。
Vector
我们大多数情况下都希望“数组”元素的个数是可变的,这时候可以使用 Vector 类型。
Vector 的使用方式如下:
1 2 3 4 5 6 7 8 9 10 11 |
|
Vector 主要有两种初始化方式:
- 使用
vec!
宏 - 使用
Vec::new()
方法
Vector在内存中由三部分组成: - 指向堆地址的指针,因为分配在堆上,所以才能动态修改大小 - 现有元素个数 - Vector 的容量(capacity),即一共可以保存多少个元素。
像其他语言一样, Rust 在创建 Vector 的时候,也可以预留一定数量的存储空间,以防止在元素增加时因元素移动、拷贝导致的开销。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
我们可以使用 Vec::with_capacity()
来预分配指定大小的存储空间,然后如果实际元素个数超过这个值时,Vector 的容量会扩展,我们看到当元素个数超过预分配的 3 之后,capacity 涨到了 6。
从上面我们也可以看到,Array 最大的一个限制是它的固定大小。与此相对,Vector 的特点是:
- 分配在堆上
- 可以在运行时动态添加或删除元素
Slice
我们再来看一下 Slice ,一般语言翻译为切片。Slice 一般是指向 Array 或 Vector 的位置,一般用 &[T]
表示。
我们可以通过使用指向 Array 或 Vector 的 range 来创建 Slice 类型的变量。Slice 在使用的时候和 Array 非常像。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
Slice 在实现上被称作 fat pointer。fat pointer 在内存中保存了两个值: - Slice 指向的位置 - Slice 包含的元素数量
Rust 中,&[T]
、&mut [T]
,、Box<[T]>
等类型在内存中占有 16 个字节,其中前 8 个字节为指针位置,后 8 个字节为 Slice 包含的元素个数,即长度。
1 2 3 |
|
而 Vec<T>
在内存中结构如下:
1 2 3 |
|
在内存中 Vec<T>
占用 24 个字节,和 Slice 相比,多了 8 个字节的空间来存储 capacity 属性。
有了静态分配大小的 Array,以及可以动态增减元素的 Vector,为什么还有一个叫做 Slice 的东西呢?按照官方文档的说明,Slice是一个指向底层 Array 或 Vector 的视图,可以实现安全、高效的数据访问而不会发生内存拷贝。一般来说我们都不会直接创建 Slice ,而是从已有的 Array 或 Vector 创建 Slice。
Slice 虽然是一个视图(view),但是也可以通过 &mut [T]
它来修改底层元素:
1 2 3 4 |
|
String and &str
最后稍微来介绍一下 String 和 &str 的关系,这有点像 Vector 和 Slice 的对应关系关系。在内存中,&str也包括指向实际数据位置的指针和长度属性。
字符串 Slice 也可以看做是 8 bit 整数 Slice 重新定义的类型,两者的对应关系如下:
1 2 3 4 |
|