<< Home

Chapter 5: Strings and Collections

Rust offers several tools for storing and organizing data. Some focus on text, some on ordered sequences, and others on fast lookup through hashing or sorted keys. Understanding how these pieces fit together helps you choose the right collection for each task and use them with confidence.

String vs str

Rust has two closely related text types. String owns a growable buffer of UTF-8 bytes on the heap; it can change size, push content and take ownership of text. str describes a UTF-8 slice that someone else owns. You usually see it as &str, which is a borrowed view into existing text. This split keeps memory handling clear and avoids hidden allocations.

Where String fits

String is your go-to type when you need to build or modify text. It can expand, shrink and hold any valid UTF-8 sequence.

let mut s = String::from("Rust");
s.push_str("ace");
println!("{s}");

This example adds more text to the owned buffer. Because String owns its memory, it can be moved, borrowed or cloned when needed.

Where &str fits

&str is a borrowed slice of text. It does not own memory and cannot grow. Most string literals behave as &str, and many functions prefer &str when they only need to read text.

let msg: &str = "hello";  // a borrowed string slice
println!("{msg}");

Because &str borrows its data, the owner must live long enough to keep the slice valid.

⚠️ Choose String when you need ownership or mutation; choose &str when you only need to read.

Vec, arrays, and slices

Rust’s fundamental sequence types come in three forms. Arrays have fixed size known at compile time; vectors grow at runtime and own their memory; slices are borrowed views into arrays or vectors. These types cover most tasks that involve ordered data.

Arrays

An array has a fixed length and a single element type. Its size is part of its type, which makes arrays predictable and fast.

let a: [i32; 3] = [10, 20, 30];
println!("{:?}", a);

Arrays live on the stack when small and are ideal for fixed structures such as coordinates or short static tables.

Vectors Vec<T>

Vec<T> is a growable list. It owns heap memory and can push, pop and resize itself freely.

let mut v = Vec::new();
v.push(1);
v.push(2);
println!("{:?}", v);

Vectors are the most common sequence type in Rust because they adapt easily to changing workloads.

Slices &[T]

A slice borrows a section of an array or vector. It does not own memory and cannot change size, but it gives safe read access to ranges of elements.

let nums = vec![5, 6, 7, 8];
let part: &[i32] = &nums[1..3];
println!("{:?}", part);

Slices are lightweight views; their lifetime is tied to the underlying collection.

💡 You can convert arrays and vectors to slices easily. Borrowing with &v yields a slice of the entire vector.

HashMap and BTreeMap

Rust provides two main dictionary types: HashMap for fast average lookups, and BTreeMap for sorted keys and predictable ordering. Both store key-value pairs and let you query, insert and update entries efficiently.

Using HashMap

HashMap stores keys based on hashes and gives near constant-time access for inserts and lookups.

use std::collections::HashMap;

let mut map = HashMap::new();
map.insert("a", 1);
map.insert("b", 2);

println!("{:?}", map.get("a"));

Hash-based maps are ideal when ordering does not matter and speed does.

Using BTreeMap

BTreeMap keeps keys in sorted order, which supports ordered iteration and range queries.

use std::collections::BTreeMap;

let mut map = BTreeMap::new();
map.insert(30, "thirty");
map.insert(10, "ten");

for (k, v) in &map {
  println!("{k}: {v}");
}

Sorted maps are helpful when you need predictable iteration or want to work with key ranges.

⚠️ HashMap is usually faster; BTreeMap stays ordered. Pick based on whether ordering or raw speed matters more.

Sets and other standard types

Sets store unique values without duplicates. Rust includes HashSet and BTreeSet, which mirror the behaviour of HashMap and BTreeMap. The standard library also offers queues, binary heaps and other specialised collections.

Using HashSet

HashSet provides quick membership tests and enforces uniqueness.

use std::collections::HashSet;

let mut set = HashSet::new();
set.insert("apple");
set.insert("banana");
set.insert("apple");  // ignored, already present

println!("{:?}", set);

This is useful for filtering duplicates or checking if something is included.

Binary heaps

BinaryHeap stores items in priority order. You always get the largest element when popping.

use std::collections::BinaryHeap;

let mut heap = BinaryHeap::new();
heap.push(10);
heap.push(4);
heap.push(7);

println!("{:?}", heap.pop());

Binary heaps are great for scheduling, priority tasks or algorithms that need a top element repeatedly.

Iteration patterns over collections

Rust’s collections expose iterators that let you traverse items by value, by reference or by mutable reference. Iterators are lazy; they produce values as needed and chain together smoothly.

Iterating by reference

This is the common pattern when you only need to read items.

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

for n in &nums {
  println!("{n}");
}

The vector is borrowed, so it remains usable after the loop.

Iterating mutably

Use &mut when you need to adjust each element in place.

let mut nums = vec![1, 2, 3];

for n in &mut nums {
  *n *= 2;
}

println!("{:?}", nums);

This pattern preserves ownership and modifies items safely.

Consuming iteration

Iterating by value takes ownership of items and consumes the collection.

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

for n in nums {
  println!("{n}");
}

The vector cannot be used afterward because all its elements have been moved.

💡 Many iterator adaptors such as map, filter and collect work across all these patterns and help you build clear pipelines of data transformations.