Master Vec, String, and HashMap, the data structures you'll use in every Rust project.
Vectors are growable arrays stored on the heap. They allow you to store multiple values of the same type.
// Creating vectors
let mut v = Vec::new();
v.push(5);
v.push(6);
v.push(7);
// Using macro shorthand
let v = vec![1, 2, 3, 4, 5];
// Accessing elements
let third = &v[2]; // Index access, panics if out of bounds
println!("Third element: {}", third);
match v.get(10) {
Some(element) => println!("Element: {}", element),
None => println!("No element at index 10"),
// Modifying vectors
let mut v = vec![1, 2, 3];
v.push(4);
v.pop(); // Returns Option<i32>
v.insert(1, 99); // Insert at index
v.remove(1); // Remove and return element
Prefer v.get(index) over v[index] when the index might be out of bounds. get() returns an Option instead of panicking.
let v = vec![1, 2, 3, 4, 5];
// Immutable iteration
for element in &v {
println!("Element: {}", element);
// Mutable iteration - modify elements
let mut v = vec![1, 2, 3];
for element in &mut v {
*element += 50; // Dereference to modify
// Taking ownership
let v = vec![1, 2, 3];
for element in v {
println!("Element: {}", element);
} // v is consumed here
// Using enumerate
let v = vec!["a", "b", "c"];
for (index, value) in v.iter().enumerate() {
println!("Index: {}, Value: {}", index, value);
}
Iterating with for element in v (without &) moves the vector. After the loop, v is no longer usable. Use &v for borrowing or &mut v for mutable access.
// Using enums to store multiple types
#[derive(Debug)]
enum SpreadsheetCell {
Int(i32),
Float(f64),
Text(String),
let row = vec![
SpreadsheetCell::Int(3),
SpreadsheetCell::Text(String::from("blue")),
SpreadsheetCell::Float(10.12),
];
for cell in &row {
match cell {
SpreadsheetCell::Int(i) => println!("Integer: {}", i),
SpreadsheetCell::Float(f) => println!("Float: {}", f),
SpreadsheetCell::Text(s) => println!("Text: {}", s),
}
}
// Using trait objects for polymorphism
trait Shape {
fn area(&self) -> f64;
struct Circle { radius: f64 }
impl Shape for Circle {
fn area(&self) -> f64 { 3.14 * self.radius * self.radius }
struct Square { side: f64 }
impl Shape for Square {
fn area(&self) -> f64 { self.side * self.side }
let shapes: Vec<Box<dyn Shape>> = vec![
Box::new(Circle { radius: 5.0 }),
Box::new(Square { side: 4.0 }),
];
for shape in shapes {
println!("Area: {}", shape.area());
}
Vectors can only hold one type, but you can work around this using enums (when you know all types at compile time) or trait objects with Box<dyn Trait> (for open-ended polymorphism).
// String literals are &str (string slices)
let s: &str = "hello";
// String is an owned, growable UTF-8 encoded string
let mut s = String::new();
s.push_str("hello");
s.push(' ');
s.push_str("world");
// Creating strings
let s1 = String::from("hello");
let s2 = "world".to_string();
// String slices
let s = String::from("hello world");
let hello = &s[0..5];
let world = &s[6..11];
// Concatenation with +
let s1 = String::from("hello");
let s2 = String::from(" world");
let s3 = s1 + &s2; // s1 moved, s2 borrowed
// println!("{}", s1); // Error: s1 moved
// Using format! macro for cleaner concatenation
let s1 = String::from("tic");
let s2 = String::from("tac");
let s3 = String::from("toe");
let s = format!("{}-{}-{}", s1, s2, s3); // All s1, s2, s3 still valid
Use format! instead of + for concatenating multiple strings. It doesn't take ownership of any of its arguments and produces cleaner code.
let s = String::from("hello");
// Bytes
for b in s.as_bytes() {
println!("Byte: {}", b);
// Characters - use chars() not indexing!
let s = "Привет"; // Russian, multi-byte chars
for c in s.chars() {
println!("Char: {}", c);
// Indexing a String panics - strings use UTF-8
// let ch = s[0]; // ERROR!
// Byte access
let hello = "Здравствуй"; // 4-byte UTF-8 chars
println!("Byte length: {}", hello.len()); // 20 bytes, not 10 chars
// Correct slicing by byte boundaries
let s = "hello";
let slice = &s[0..4]; // "hell" - valid boundary
// Safe iteration
let word = "Привет";
let first_char = word.chars().next().unwrap(); // 'П'
let char_count = word.chars().count(); // 6
let byte_count = word.len(); // 12 (UTF-8 encoded)
// Grapheme clusters (combining characters)
let s = "e\u{0301}"; // e with accent
println!("Char count: {}", s.chars().count()); // 2
println!("String: {}", s); // Appears as 1 character
You cannot index into a Rust String with s[0]. Rust strings are UTF-8 encoded, and a single character may span multiple bytes. Use .chars(), .bytes(), or byte-boundary slicing instead.
use std::collections::HashMap;
// Creating a new HashMap
let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);
// Getting values
let team = String::from("Blue");
let score = scores.get(&team);
match score {
Some(s) => println!("Score: {}", s),
None => println!("Team not found"),
// Using unwrap_or
let score = scores.get(&String::from("Red")).unwrap_or(&0);
// Iterating over HashMap
for (key, value) in &scores {
println!("{}: {}", key, value);
// Creating HashMap from vectors
let teams = vec![String::from("Blue"), String::from("Yellow")];
let initial_scores = vec![10, 50];
let scores: HashMap<_, _> = teams.into_iter()
.zip(initial_scores.into_iter())
.collect();
use std::collections::HashMap;
let mut map = HashMap::new();
// Overwriting a value
map.insert("a", 10);
map.insert("a", 25); // "a" now maps to 25
// Only insert if key doesn't exist
map.entry("a").or_insert(50); // "a" still 25
map.entry("b").or_insert(50); // "b" now 50
// Updating based on old value
let mut map = HashMap::new();
map.insert("word", 1);
for word in vec!["word", "hello", "word"] {
let count = map.entry(word).or_insert(0);
*count += 1;
// word: 2, hello: 1
// Entry API for complex updates
let mut map = HashMap::new();
let entry = map.entry(String::from("key"));
let value = entry.or_insert(vec![]);
value.push(1);
// Getting mutable reference
let mut map = HashMap::new();
map.insert(String::from("key"), 5);
if let Some(v) = map.get_mut(&String::from("key")) {
*v = 10;
}
The Entry API (map.entry(key).or_insert(default)) is a powerful pattern that avoids double lookups. It checks for a key and inserts a default in one step, returning a mutable reference to the value.
use std::collections::HashMap;
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
struct Person {
name: String,
age: u32,
// Using custom types as keys
let mut people: HashMap<Person, String> = HashMap::new();
let alice = Person {
name: String::from("Alice"),
age: 30,
};
people.insert(alice, String::from("Engineer"));
// Word frequency counter
fn count_words(text: &str) -> HashMap<&str, u32> {
let mut map = HashMap::new();
for word in text.split_whitespace() {
let count = map.entry(word).or_insert(0);
*count += 1;
}
map
let text = "hello world hello rust rust rust";
let frequencies = count_words(text);
for (word, count) in frequencies {
println!("{}: {}", word, count);
}
// Grouping values by key
let mut groups: HashMap<char, Vec<u32>> = HashMap::new();
let data = vec![(1, 'a'), (2, 'b'), (3, 'a'), (4, 'c'), (5, 'b')];
for (num, letter) in data {
groups.entry(letter).or_insert_with(Vec::new).push(num);
for (letter, nums) in groups {
println!("{}: {:?}", letter, nums);
}
To use a custom struct as a HashMap key, derive Eq, PartialEq, and Hash. Rust requires keys to be hashable and comparable for equality.