DEV Community

Cover image for [Rust Guide] 8.1. Vector
SomeB1oody
SomeB1oody

Posted on

[Rust Guide] 8.1. Vector

8.1.0. Chapter Overview

Chapter 8 is mainly about common collections in Rust. Rust provides many collection-like data structures, and these collections can hold many values. However, the collections covered in Chapter 8 are different from arrays and tuples.

The collections in Chapter 8 are stored on the heap rather than on the stack. That also means their size does not need to be known at compile time; at runtime, they can grow or shrink dynamically.

This chapter focuses on three collections: Vector (this article), String, and HashMap.

If you find this helpful, please like, bookmark, and follow. To keep learning along, follow this series.

8.1.1. Using Vector to Store Multiple Values

Vector is written as Vec<T>, where T represents a generic type parameter that can be replaced with the desired data type during actual use.

Vector is provided by the standard library. It stores multiple values of the same type contiguously in memory. You can think of it as a resizable array.

To create a Vector, use the Vec::new function. See the example:

fn main() {  
    let v: Vec<i32> = Vec::new();
    let v = vec![1, 2, 3];
    let v = Vec::with_capacity(10);
}
Enter fullscreen mode Exit fullscreen mode
  • let v: Vec<i32> = Vec::new(): Declares a Vector with i32 elements using Vec::new (commonly used).
  • let v = vec![1, 2, 3]: Creates a Vector with initial values using the vec! macro. Here, 1, 2, 3 are inserted into the vector. Using vec![] with no content is also valid (commonly used).
  • let v = Vec::with_capacity(10): Creates an empty vector with pre-allocated capacity for at least 10 elements. Suitable when you know the approximate number of elements, reducing reallocations and improving performance.

The first method (Vec::new()) requires explicit type annotation (Vec<i32>) because it creates an empty vector with no elements. Without contextual information for Rust to infer the type, it would cause an error. With context, Rust can infer the element type.

The second method (vec![]) doesnโ€™t require explicit type annotation because the Rust compiler infers the element type (i32) from the initial values.

8.1.2. Updating a Vector

1. Adding Elements

Use the push method to add elements to the end of a vector:

fn main() {  
    let mut v = Vec::new();  
    v.push(1);  
}
Enter fullscreen mode Exit fullscreen mode
  • Note: The vector must be mutable (declared with mut) to add elements.
  • In let mut v = Vec::new();, the element type is inferred as i32 from the subsequent push(1) operation.

Other methods for adding elements:

fn main() {  
    let mut v = Vec::new();  
    v.extend([1, 2, 3]);  // Batch insertion
    v.insert(1, 99);      // Insert at index (panics if out-of-bounds)

    let mut a = vec![1, 2, 3];
    let mut b = vec![4, 5, 6];
    a.append(&mut b);      // Moves all elements from `b` to `a` (empties `b`)
}
Enter fullscreen mode Exit fullscreen mode

2. Removing Elements from a Vector

pop(): Removes and returns the last element wrapped in Option (covered in 6.2. The Option Enum). Returns None if empty.

fn main() {  
    let mut v = vec![1, 2, 3];
    let x = v.pop(); // Returns Some(3)
}
Enter fullscreen mode Exit fullscreen mode

remove(index): Deletes the element at the specified index and returns it. Shifts subsequent elements left. Panics if index is invalid.

fn main() {  
    let mut v = vec![1, 2, 3];
    let x = v.remove(1); // Returns 2 (v becomes [1, 3])
}
Enter fullscreen mode Exit fullscreen mode

clear(): Removes all elements. Length becomes 0, but capacity remains.

fn main() {  
    let mut v = vec![1, 2, 3];
    v.clear(); // v is now []
}
Enter fullscreen mode Exit fullscreen mode

Like any struct, when a Vector goes out of scope, it and its elements are automatically cleaned up.

3. Reading Elements of a Vector

There are two ways to access values in a Vector: using indexing or the get method. For example, given a vector containing [1, 2, 3, 4, 5], access and print the third element:

fn main() {  
    let v = vec![1, 2, 3, 4, 5];  
    let third = &v[2];  // Indexing
    println!("The third element is {}", third);  

    match v.get(2) {    // get method with match
        Some(third) => println!("The third element is {}", third),  
        None => println!("There is no third element."),  
    };  
}
Enter fullscreen mode Exit fullscreen mode
  • let third = &v[2];: Uses indexing to access the element at position 2 (third element). The & indicates a reference.
  • v.get(2): Uses the get method for access. Since it returns an Option type, we use match (covered in 6.3. The Match Control Flow Operator) to unpack it. If a value exists, it binds to third and prints; if not (None), it prints "There is no third element."

Both methods achieve the same result but handle invalid access (e.g., out-of-bounds index) differently.

Testing with indexing (invalid access):

fn main() {
    let v = vec![1, 2, 3, 4, 5];
    let third = &v[100]; // Index 100 is out-of-bounds
    println!("The third element is {}", third);
}
Enter fullscreen mode Exit fullscreen mode

Output:

index out of bounds: the len is 5 but the index is 100
Enter fullscreen mode Exit fullscreen mode

The program triggers panic! and terminates.

Testing with get (invalid access):

fn main() {
    let v = vec![1, 2, 3, 4, 5];
    match v.get(100) {  // Index 100 is out-of-bounds
        Some(third) => println!("The third element is {}", third),  
        None => println!("There is no third element."),  
    };  
}
Enter fullscreen mode Exit fullscreen mode

Output:

There is no third element.
Enter fullscreen mode Exit fullscreen mode

Since get cannot access index 100, it returns None.

Guideline: Use indexing when out-of-bounds access should terminate the program via panic!. Otherwise, prefer get for safe handling.

8.1.3. Ownership and Borrowing Rules

Remember the borrowing rule discussed in Chapter 4.2. Ownership Rules, Memory, and Allocation? You cannot have mutable and immutable references in the same scope at the same time. This rule still applies to Vector. Example:

fn main() {
    let mut v = vec![1, 2, 3, 4, 5];
    let first = &v[0];
    v.push(6);
    println!("The first element is {}", first);
}
Enter fullscreen mode Exit fullscreen mode

Output:

error[E0502]: cannot borrow `v` as mutable because it is also borrowed as immutable
 --> src/main.rs:4:5
  |
3 |     let first = &v[0];
  |                  - immutable borrow occurs here
4 |     v.push(6);
  |     ^^^^^^^^^ mutable borrow occurs here
5 |     println!("The first element is {}", first);
  |                                         ----- immutable borrow later used here
Enter fullscreen mode Exit fullscreen mode
  • The push function has the signature &mut self, value: T. &mut means that push treats the passed-in variable as a mutable reference. In the example, v is used as a mutable reference here.
  • let first = &v[0]; makes first an immutable reference to v. Because the two references exist in the same scope, an error is produced.
  • println! treats the values passed to it as immutable references.

Because mutable and immutable references appear at the same time in this scope, the program fails to compile.

Someone might wonder: push adds things to the end of a Vector, and the earlier elements are not affected. Why does Rust make this so complicated?

That is because the elements of a Vector are stored contiguously in memory. If you add an element to the end and there happens to be something occupying the space after it, there may be no room for the new element. In that case, the system must reallocate memory and find a large enough area to hold the Vector after the new element is added. When that happens, the original memory block may be freed or reallocated, but the reference still points to the old memory address, creating a dangling reference (discussed in Chapter 4.4, Reference and Borrowing).

8.1.4. Iterating Over Values in a Vector

Using a for loop is the most common approach. Example:

fn main() {  
    let v = vec![1, 2, 3, 4, 5];  
    for i in &v {  
        println!("{}", i);  
    }  
}
Enter fullscreen mode Exit fullscreen mode

Output:

1
2
3
4
5
Enter fullscreen mode Exit fullscreen mode

Of course, if you want to modify elements inside the loop, that is also possible. You only need to make v mutable and change &v to &mut v:

fn main() {  
    let mut v = vec![1, 2, 3, 4, 5];  
    for i in &mut v {  
        *i += 10;  
    }  
    for i in v {  
        println!("{}", i);  
    }  
}
Enter fullscreen mode Exit fullscreen mode

Note: the * in front of *i on the fourth line is there because i is essentially of type &mut i32. It stores a pointer rather than the actual i32 value, so you need to dereference it first and turn i into an mut i32 value to get the actual number before you can perform addition or subtraction.

Output:

11
12
13
14
15
Enter fullscreen mode Exit fullscreen mode

Top comments (0)