Jump to content

Vectors

From JOHNWICK

After getting cozy with enums and pattern matching in our previous days, it’s time to turn our attention to another Rust superstar: vectors. If you missed yesterday’s writeup, you can check out the link below.

Pattern Matching with Enums Yesterday, we looked into Rust enums, seeing how they can be used for modelling choices, states, and even data-packed… medium.com

If enums are about choosing between distinct options, vectors are about gathering a bunch of items in one place, ready to grow or shrink as needed.

Think of vectors as a dynamic backpack for your data, unlike the fixed-size shoebox of arrays. In this guide, we’ll discuss what makes vectors useful, how they differ from arrays, and how to use them in fun, practical ways. Let’s start by getting to know vectors and their place in Rust.

What Are Vectors?

Vectors in Rust, written as Vec<T>, are like a stretchy list that can hold any number of items of the same type. Need to store a collection of scores, names, or game items? A vector’s is all you need, and it can expand or contract as your program runs.

Unlike some other languages where lists might feel like a free-for-all, Rust’s vectors are tightly managed, ensuring safety and performance.

You create a vector with Vec::new() or the handy vec! macro, like this:

// Empty vector
let scores: Vec<i32> = Vec::new();

// Vector with initial values
let names = vec!["Alice", "Bob", "Charlie"];

Here, scores is an empty vector ready to hold i32 numbers, while names starts with three strings. The T in Vec<T> means you can store any type, as long as all elements match that type. But how do vectors compare to arrays, which we’ve all seen before? Let’s clear that up next.

Vectors vs. Arrays Key Differences

Arrays and vectors might seem like cousins, but they’re built for different jobs. An array in Rust, like [i32; 3], is a fixed-size collection, like a shelf with exactly three slots, no more, no less. Once you set its size, it’s locked in. Vectors, on the other hand, are dynamic, growing or shrinking as you add or remove items. Here’s a quick comparison:

let array = [1, 2, 3]; // Fixed size: 3 elements

let mut vector = vec![1, 2, 3]; // Can grow or shrink
vector.push(4); // Vector now has 4 elements
// array.push(4); // Nope, arrays can’t do this!

Arrays live on the stack, making them super fast for small, fixed collections, but they’re rigid. Vectors live on the heap, which gives them the flexibility to change size but comes with a slight performance cost. Arrays are great for things like RGB colors ([u8; 3]) where the size never changes, while vectors shine for lists that evolve, like a player’s inventory in a game. With this distinction in mind, let’s see how to create and fill vectors.

Creating and Populating Vectors

Vectors are easy to set up, and Rust gives you a few ways to get started. You can create an empty vector with Vec::new() or use the vec! macro for instant initialization:

let mut numbers: Vec<i32> = Vec::new(); // Empty, ready for action
numbers.push(10); // Add one element
numbers.push(20); // Add another

let fruits = vec!["apple", "banana", "orange"]; // Pre-filled vector

The mut keyword is necessary if you want to modify the vector, like adding elements with push. You can also create a vector with a specific capacity using Vec::with_capacity(n) to optimize performance if you know roughly how many items you’ll store. For example:

let mut buffer: Vec<u8> = Vec::with_capacity(100); // Room for 100 bytes

Once your vector is ready, you’ll want to add, remove, or access elements. Let’s check out the common operations that make vectors so handy.

Common Vector Operations

Vectors come with various methods to manage your data. Here are some of the most useful ones, using a game inventory as an example:

let mut inventory = vec!["sword", "shield", "potion"];
inventory.push("ring"); // Add "ring" to the end

inventory.pop(); // Remove and return "ring"

inventory[1] = "armor"; // Replace "shield" with "armor"

let first = inventory.get(0); // Get "sword" as Option<&str>

  • Push and Pop: push adds an item to the end, while pop removes and returns the last item, returning an Option<T> (Some(item) or None if empty).
  • Indexing — Use inventory[i] to access or modify an element directly, but beware—Rust will panic if the index is out of bounds.
  • Safe Access: get(i) is safer, returning Some(&item) or None if the index doesn’t exist.
  • Length and Capacity: Check len() for the current number of elements and capacity() for the allocated space.

You can also iterate over a vector to process each item:

for item in &inventory {
    println!("Found item: {}", item);
}

This loop borrows each item, keeping the vector intact. If you need to modify elements while iterating, use &mut inventory.

These operations make vectors flexible for all sorts of tasks, but what happens when you need to work with vectors in a real program? Let’s look at some practical examples.

Vectors in Real-World Scenarios

Vectors are perfect for dynamic scenarios where data grows or changes. Imagine a game where players collect items in their inventory:

let mut inventory = vec!["sword", "shield"];
inventory.push("potion");

if inventory.len() < 5 {
    println!("Inventory not full yet, {} slots left!", 5 - inventory.len());
} else {
    println!("Inventory full!");
}

Here, the vector grows as the player collects items, and the code checks if the inventory is full. Another example is a high-score list:

let mut scores = vec![100, 200, 150];
scores.sort(); // Sort in ascending order

println!("Top score: {}", scores.last().unwrap());

Vectors make it easy to sort, filter, or manipulate lists dynamically. In a server log, you might store incoming requests:

let mut requests = Vec::new();
requests.push("GET /home");
requests.push("POST /login");

for req in requests.iter().rev() {
    println!("Processing request: {}", req);
}

These examples show how vectors adapt to changing data, unlike arrays, which stay fixed. But to use vectors effectively, you need to follow some best practices.

Best Practices for Using Vectors

  • To make vectors work smoothly in your code, keep these tips in mind. Pre-allocate capacity with Vec::with_capacity if you know the approximate size to avoid frequent reallocations, which can slow things down. For example:

let mut big_list: Vec<i32> = Vec::with_capacity(1000);

  • Use get instead of direct indexing when accessing elements to avoid panics, especially with user input.
  • When iterating, decide whether you need ownership, borrowing (&vec), or mutable borrowing (&mut vec) based on your needs. Also, consider draining or clearing a vector instead of creating a new one if you’re reusing it:

inventory.clear(); // Empty the vector, keep the capacity

  • Finally, if you’re passing vectors between functions, passing a reference (&Vec<T>) is often more efficient than cloning the whole vector. These habits keep your code fast and safe. Speaking of safety, let’s talk about some common pitfalls to avoid.

Common Pitfalls and How to Avoid Them

Vectors are forgiving, but there are a few traps to watch out for. Indexing out of bounds is a big one:

let v = vec![1, 2, 3];

println!("{}", v[10]); // Panic! Out of bounds Use get or check len first:

if v.len() > 10 {
    println!("{}", v[10]);
} else {
    println!("Index too high!");
}

Another mistake is forgetting to make a vector mutable when you need to modify it:

let v = vec![1, 2, 3];
v.push(4); // Error: v is not mutable

Always add mut when declaring a vector you plan to change. Also, avoid unnecessary cloning of large vectors, as it can be slow, use references or slices instead. Finally, don’t confuse vectors with arrays when planning your data structure. If the size is fixed and small, an array might be simpler and faster.

Read the full article here: https://medium.com/rustaceans/vectors-9d14cbaa693f