Regardless of the programming language, the most common data types are numeric, string, and array. Here array is a general term, generally refers to a collection that can hold multiple elements, but of course the collection here is not strictly mathematical definition.

Array

Let’s look at arrays first.

An array is a collection of data of the same type, located in contiguous blocks of memory, and stored on the stack rather than the heap.

The following are the basic ways to use arrays.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
fn main() {
    let a = [1, 2, 3, 4];
    assert_eq!(1, a[0]);

    let a: [i32; 4] = [1, 2, 3, 4];
    assert_eq!(4, a[3]);

    let a = [0; 4];
    assert_eq!(0, a[2]);

    let mut a = [1, 2, 3];
    assert_eq!(1, a[0]);

    a[0] = 11;
    assert_eq!(11, a[0]);
}

There are 3 main ways to create arrays.

  • [V]: use the element values directly, without specifying the array type, e.g. let a = [1, 2, 3, 4]
  • [T; N]: declares an array of type T of length N, e.g. let a: [i32; 4]
  • [V; N]: declares an array with each element of value V and length N, e.g. let a: [0; 4] . The type of the array is of course the type T of V.

If we use the mutable keyword mut in the declaration, it means that the values of the elements of the array can be modified.

1
2
let mut a = [1, 2, 3];
a[0] = 11;

The most important feature of arrays in Rust is that they are immutable in size, which can be difficult for some programming language users to understand, meaning that while we can change the value of an element in an array, we cannot add elements to it or delete elements from it.

To summarize, arrays are assigned a size at compile time, stored on the stack, and the length of the array is immutable, although we can change the value of an element in the array.

Vector

Most of the time we want the number of elements of an “array” to be variable, so we can use the Vector type.

Vector is used in the following way.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
fn main() {
    let v = vec![1, 2, 3];
    assert_eq!(1, v[0]);

    let v = vec![4; 3];
    assert_eq!(4, v[0]);

    let mut v: Vec<i32> = Vec::new();
    v.push(5);
    assert_eq!(5, v[0]);
}

There are two main ways to initialize a Vector.

  • using the vec! macro
  • using the Vec::new() method

A Vector consists of three parts in memory: - a pointer to the heap address, which can be dynamically modified because it is allocated on the heap - the number of elements available - the capacity of the Vector, i.e. how many elements can be stored in total.

Like other languages, Rust can reserve a certain amount of storage space when creating a Vector to prevent the overhead of moving and copying elements as they are added.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
let mut v: Vec<i32> = Vec::with_capacity(3);
assert_eq!(0, v.len());
assert_eq!(3, v.capacity());

v.push(1);
assert_eq!(1, v.len());
assert_eq!(3, v.capacity());
v.push(2);
v.push(3);
assert_eq!(3, v.len());
assert_eq!(3, v.capacity());

v.push(4);
assert_eq!(4, v.len());
assert_eq!(6, v.capacity());

We can use Vec::with_capacity() to preallocate a specified size of storage space, and then if the actual number of elements exceeds this value, the capacity of the Vector will expand, and we see that when the number of elements exceeds the preallocated 3, the capacity rises to 6.

As we can see from the above, one of the biggest limitations of Array is its fixed size. In contrast, the Vector has the following characteristics.

  • is allocated on the heap
  • elements can be added or removed dynamically at runtime

Slice

Let’s take a look at Slice, which translates to slice in common language. slice is generally a location pointing to an Array or Vector, usually denoted by &[T].

We can create variables of type Slice by using a range that points to an Array or Vector. slice is very much like Array when used.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
fn main() {
    let a = [1, 2, 3, 4, 5];
    let v = vec![1, 2, 3, 4, 5];

    let sa: &[i32] = &a;
    let sv: &[i32] = &v;

    assert_eq!([1, 2, 3, 4, 5], sa);
    assert_eq!([1, 2, 3, 4, 5], sv);

    let sa: &[i32] = &a[1..3];
    assert_eq!([2, 3], sa);

    let sa: &[i32] = &a[1..=3];
    assert_eq!([2, 3, 4], sa);
}

Slice is called fat pointer in the implementation. fat pointer holds two values in memory: - the position to which the Slice refers - the number of elements the Slice contains

In Rust, types &[T], &mut [T], Box<[T]>, etc. occupy 16 bytes in memory, the first 8 bytes of which are the location of the pointer, and the last 8 bytes are the number of elements contained in the Slice, i.e., its length.

1
2
3
 0-7 | 8-15
-----|------
 ptr | len

And Vec<T> is structured in memory as follows.

1
2
3
 0-7 | 8-15 | 16-23
-----|------|-------
 ptr | cap  |  len

In memory Vec<T> takes up 24 bytes, which is 8 bytes more space to store the capacity property compared to Slice.

Why is there something called Slice when there is an Array with statically allocated size and a Vector that can dynamically add and remove elements? According to the official documentation, a Slice is a view to the underlying Array or Vector that allows safe and efficient data access without memory copies. Normally we don’t create a Slice directly, but from an existing Array or Vector.

Although a Slice is a view, it can be modified by &mut [T].

1
2
3
4
let mut v = vec![1, 2, 3, 4, 5];
let sv: &mut [i32] = &mut v;
sv[1] = 9;
assert_eq!(9, sv[1]);

String and &str

Finally, a little about the relationship between String and &str, which is a bit like the relationship between Vector and Slice. In memory, &str also includes a pointer to the actual data location and a length attribute.

String Slice can also be thought of as a redefined type of 8 bit integer Slice, and the correspondence between the two is as follows.

1
2
3
4
5

Byte slice | 字符串 slice
------------|-------------
 &[u8]      | &str
 Vec<u8>    | String