You got a taste of Vec in Lesson 06. Now let's dig deeper into Vec, HashMap, and String — the three most important collections.
Working Code
use std::collections::HashMap;
fn main() {
// Vec — ordered list
let mut scores = vec![85, 92, 78];
scores.push(95);
println!("Scores: {:?}", scores);
// HashMap — key-value store
let mut contacts = HashMap::new();
contacts.insert("Alice", "555-1234");
contacts.insert("Bob", "555-5678");
println!("Alice: {}", contacts["Alice"]);
// String — UTF-8 text
let mut greeting = String::from("hel");
greeting.push_str("lo!");
println!("{greeting}");
}
Vec<T> In Depth
Useful Methods
fn main() {
let mut v = vec![10, 20, 30, 40, 50];
// insert — insert at a specific position
v.insert(2, 25); // insert 25 at index 2
println!("After insert: {:?}", v); // [10, 20, 25, 30, 40, 50]
// remove — remove at a specific position (returns the removed value)
let removed = v.remove(0);
println!("Removed: {removed}, rest: {:?}", v); // 10, [20, 25, 30, 40, 50]
// retain — keep only elements matching a condition
v.retain(|x| *x >= 30);
println!("After retain: {:?}", v); // [30, 40, 50]
// split_off — split at a position
let mut a = vec![1, 2, 3, 4, 5];
let b = a.split_off(3);
println!("a: {:?}, b: {:?}", a, b); // a: [1, 2, 3], b: [4, 5]
// contains — check membership
println!("Contains 3? {}", a.contains(&3)); // true
// sort, dedup — sort and remove duplicates
let mut nums = vec![3, 1, 4, 1, 5, 9, 2, 6, 5, 3];
nums.sort();
nums.dedup();
println!("Sorted + deduped: {:?}", nums); // [1, 2, 3, 4, 5, 6, 9]
}
Safe Access: get() vs Indexing
fn main() {
let v = vec![10, 20, 30];
// Index access — panics if out of bounds
println!("{}", v[1]); // 20
// println!("{}", v[10]); // panic!
// get() — returns Option for safety
match v.get(10) {
Some(val) => println!("Value: {val}"),
None => println!("Index out of bounds"),
}
}
HashMap<K, V>
Basic Usage
use std::collections::HashMap;
fn main() {
let mut scores: HashMap<String, i32> = HashMap::new();
// Insert
scores.insert(String::from("Math"), 95);
scores.insert(String::from("English"), 87);
scores.insert(String::from("Science"), 92);
// Lookup — get() returns Option<&V>
match scores.get("Math") {
Some(score) => println!("Math: {score}"),
None => println!("No math score"),
}
// Iterate
for (subject, score) in &scores {
println!("{subject}: {score}");
}
// Remove
scores.remove("English");
println!("After removing English: {:?}", scores);
}
The entry API — Idiomatic "Insert If Missing"
A common pattern: "insert a default if the key doesn't exist; use the existing value if it does." The entry() API handles this cleanly:
use std::collections::HashMap;
fn main() {
let text = "hello world hello rust hello";
let mut word_count: HashMap<&str, i32> = HashMap::new();
// entry API — one line does it all!
for word in text.split_whitespace() {
let count = word_count.entry(word).or_insert(0);
*count += 1;
}
println!("{:?}", word_count);
// {"hello": 3, "world": 1, "rust": 1}
}
entry(key).or_insert(default) inserts the default if the key is missing, then returns a mutable reference to the value.
Creating From Arrays
use std::collections::HashMap;
fn main() {
// From an array
let scores: HashMap<&str, i32> = HashMap::from([
("Math", 95),
("English", 87),
("Science", 92),
]);
println!("{:?}", scores);
// From iterators with collect()
let names = vec!["Alice", "Bob", "Carol"];
let ages = vec![25, 30, 28];
let people: HashMap<&str, i32> = names
.iter()
.zip(ages.iter())
.map(|(n, a)| (*n, *a))
.collect();
println!("{:?}", people);
}
String and UTF-8
String Doesn't Support Indexing!
In most languages, s[0] gets the first character. In Rust, it doesn't work:
fn main() {
let hello = String::from("hello");
// let first = hello[0]; // compile error!
println!("Byte length: {}", hello.len()); // 5
}
"Why?" — Because of UTF-8
String is internally a sequence of UTF-8 bytes. Some characters take more than one byte. For example, Korean characters use 3 bytes each:
fn main() {
let s = String::from("abc");
// View as bytes — 3 bytes
println!("Bytes: {:?}", s.as_bytes());
// View as chars — 3 characters
for c in s.chars() {
println!("Char: {c}");
}
// Get the first character
let first = s.chars().next().unwrap();
println!("First char: {first}"); // a
// Slicing works on byte boundaries only
let slice = &s[0..2]; // "ab" (2 bytes)
println!("Slice: {slice}");
// let bad = &s[0..1]; // works for ASCII, but be careful with multi-byte chars!
}
| Expression | "hello" | "abc" |
| ------------------------------- | ----------- | ----------- |
| .len() (bytes) | 5 | 3 |
| .chars().count() (characters) | 5 | 3 |
| s[0] | Not allowed | Not allowed |
| .chars().next() | Some('h') | Some('a') |
Creating and Joining Strings
fn main() {
// Multiple ways to create
let s1 = String::from("hello");
let s2 = "hello".to_string();
let s3 = format!("Hello, {}!", "world");
// Joining
let greeting = format!("{} {}", s1, s2);
println!("{greeting}");
// Appending
let mut s = String::from("hello");
s.push_str(", world");
s.push('!'); // single char
println!("{s}");
}
Collections and Ownership
Be mindful of how ownership works with collections.
Pushing Into a Vec Moves Ownership
fn main() {
let name = String::from("Alice");
let mut names = Vec::new();
names.push(name);
// println!("{name}"); // error! name moved into Vec
// Solution: store a clone, or use references
let name2 = String::from("Bob");
names.push(name2.clone()); // push a clone
println!("{name2}"); // original still alive
}
HashMap Key Ownership
use std::collections::HashMap;
fn main() {
let key = String::from("name");
let value = String::from("Alice");
let mut map = HashMap::new();
map.insert(key, value);
// println!("{key}"); // error! ownership moved to HashMap
// println!("{value}"); // error! same
// Using &str as keys avoids ownership issues
let mut map2: HashMap<&str, &str> = HashMap::new();
let k = "name";
map2.insert(k, "Alice");
println!("{k}"); // OK! &str has the Copy trait
}
Borrowing Rules Apply
fn main() {
let mut v = vec![1, 2, 3, 4, 5];
let first = &v[0]; // immutable reference
// v.push(6); // error! can't modify while immutable ref exists
println!("First: {first}");
// After first is no longer used, it's OK
v.push(6); // OK
println!("{:?}", v);
}
The borrowing rules from Lesson 14 apply here! When
Vecreallocates memory duringpush(), existing references could become invalid — that's why Rust blocks it.
Exercise 1. Use the entry API to count how many times each character appears in a sentence (excluding spaces).
use std::collections::HashMap;
fn char_count(text: &str) -> HashMap<char, usize> {
// Complete this
// Hint: iterate with text.chars(), skip spaces
}
fn main() {
let counts = char_count("hello world");
println!("{:?}", counts);
// {'h': 1, 'e': 1, 'l': 3, 'o': 2, 'w': 1, 'r': 1, 'd': 1}
}
Exercise 2. Complete the function to return the first N characters of a string.
fn take_chars(s: &str, n: usize) -> String {
// Complete this
// Hint: s.chars().take(n).collect()
}
fn main() {
let text = "Hello, Rust!";
println!("{}", take_chars(text, 5)); // "Hello"
println!("{}", take_chars(text, 20)); // "Hello, Rust!"
}
Exercise 3 (challenge). Build a student score tracker using HashMap<String, Vec<i32>>. Complete the add_score and average functions.
use std::collections::HashMap;
fn add_score(records: &mut HashMap<String, Vec<i32>>, name: &str, score: i32) {
// Complete this
// Hint: entry API + or_insert_with(Vec::new)
}
fn average(records: &HashMap<String, Vec<i32>>, name: &str) -> Option<f64> {
// Complete this
// Return None if no scores exist
}
fn main() {
let mut records = HashMap::new();
add_score(&mut records, "Alice", 90);
add_score(&mut records, "Alice", 85);
add_score(&mut records, "Bob", 95);
println!("Alice avg: {:?}", average(&records, "Alice")); // Some(87.5)
println!("Bob avg: {:?}", average(&records, "Bob")); // Some(95.0)
println!("Carol avg: {:?}", average(&records, "Carol")); // None
}
Q1. Why can't you index a String with s[0] in Rust?
- A) String is immutable
- B) It was banned for performance reasons
- C) UTF-8 encoding means a single character can span multiple bytes
- D) Rust has no character type
Q2. What does HashMap's entry(key).or_insert(default) do?
- A) Always overwrites with the default
- B) Returns an error if the key exists
- C) Inserts the default if the key is missing, then returns a mutable reference to the value
- D) Deletes the key and re-inserts
Q3. Why does this code produce an error?
fn main() {
let name = String::from("Alice");
let mut names = Vec::new();
names.push(name);
println!("{name}");
}
- A) Vec can't hold String values
- B) names isn't mutable
- C) push() moves name's ownership into the Vec, so it's no longer usable
- D) println! can't accept String