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);
}
-
let v: Vec<i32> = Vec::new(): Declares aVectorwithi32elements usingVec::new(commonly used). -
let v = vec![1, 2, 3]: Creates aVectorwith initial values using thevec!macro. Here,1, 2, 3are inserted into the vector. Usingvec![]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);
}
-
Note: The vector must be mutable (declared with
mut) to add elements. - In
let mut v = Vec::new();, the element type is inferred asi32from the subsequentpush(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`)
}
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)
}
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])
}
clear(): Removes all elements. Length becomes 0, but capacity remains.
fn main() {
let mut v = vec![1, 2, 3];
v.clear(); // v is now []
}
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."),
};
}
-
let third = &v[2];: Uses indexing to access the element at position 2 (third element). The&indicates a reference. -
v.get(2): Uses thegetmethod for access. Since it returns anOptiontype, we usematch(covered in 6.3. The Match Control Flow Operator) to unpack it. If a value exists, it binds tothirdand 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);
}
Output:
index out of bounds: the len is 5 but the index is 100
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."),
};
}
Output:
There is no third element.
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);
}
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
- The
pushfunction has the signature&mut self, value: T.&mutmeans thatpushtreats the passed-in variable as a mutable reference. In the example,vis used as a mutable reference here. -
let first = &v[0];makesfirstan immutable reference tov. 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);
}
}
Output:
1
2
3
4
5
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);
}
}
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
Top comments (0)