Collections and String Handling in Rust
Learn when to use vectors, hash maps, strings, and string slices so you can store, search, and transform data cleanly in Rust programs.
Up to this point, most of your Rust examples have been small.
A few variables.
A few structs.
A few enums.
That is enough to learn syntax and state modeling, but it is not enough to build programs that hold more than one thing.
Sooner or later, you need to answer questions like these:
- how do I store many transactions
- how do I look up one account by key
- how do I keep a growing history
- how do I parse text input without fighting ownership
That is what this lesson is for.
The mental model is simple:
- use
Vec<T>when order matters and you have many items - use
HashMap<K, V>when fast lookup by key matters - use
Stringwhen you need owned, growable text - use
&strwhen you only need to borrow text temporarily
Collections solve storage problems. Strings solve text ownership problems. Most real Rust programs need both at the same time.
Start With Vec<T>
A vector is the default collection in Rust.
Use it when:
- you want to keep items in order
- you expect the list to grow or shrink
- you want to iterate through everything
fn main() {
let mut balances = Vec::new();
balances.push(500);
balances.push(1_200);
balances.push(900);
println!("all balances: {:?}", balances);
}This is the right starting point because vectors are simple:
- one type of item
- one ordered list
- easy iteration
The Two Main Ways To Read From A Vector
fn main() {
let balances = vec![500, 1_200, 900];
let first = balances[0];
println!("first balance: {}", first);
let maybe_second = balances.get(1);
println!("second balance: {:?}", maybe_second);
}These two access patterns are not equally safe.
balances[0]will panic if the index does not existbalances.get(1)returnsOption<&T>
For beginner code, get teaches the better habit because it forces you to handle the missing case.
fn main() {
let balances = vec![500, 1_200, 900];
match balances.get(3) {
Some(balance) => println!("found balance: {}", balance),
None => println!("no balance at that index"),
}
}Iterating Over A Vector
Once you have many items, iteration becomes the normal pattern.
fn main() {
let balances = vec![500, 1_200, 900];
for balance in &balances {
println!("balance: {}", balance);
}
}Use &balances when you only need to read.
Use &mut balances when you want to change each item.
fn main() {
let mut balances = vec![500, 1_200, 900];
for balance in &mut balances {
*balance += 100;
}
println!("updated balances: {:?}", balances);
}That *balance matters because the loop variable is a mutable reference, not the number itself.
A Better Vector Example
Vectors become more useful when they store structs instead of raw numbers.
#[derive(Debug)]
struct Transfer {
to: String,
amount: u64,
}
fn main() {
let history = vec![
Transfer {
to: String::from("alice"),
amount: 500,
},
Transfer {
to: String::from("bob"),
amount: 1_200,
},
];
for transfer in &history {
println!("sent {} lamports to {}", transfer.amount, transfer.to);
}
}Now the vector is not just "a list."
It is a transaction history.
That is the right way to think about collections.
When Vec<T> Stops Being Enough
A vector is good when you want to scan through items.
It is weaker when you need to find one thing by key over and over.
For example, if you have many accounts and you want to look up one by address, this becomes awkward:
#[derive(Debug)]
struct Account {
address: String,
balance: u64,
}
fn find_account<'a>(accounts: &'a [Account], address: &str) -> Option<&'a Account> {
accounts.iter().find(|account| account.address == address)
}This works.
But every lookup scans the list.
If keyed lookup is the main operation, HashMap is a better fit.
Use HashMap<K, V> For Keyed Lookup
use std::collections::HashMap;
fn main() {
let mut balances = HashMap::new();
balances.insert(String::from("alice"), 1_500);
balances.insert(String::from("bob"), 900);
println!("alice balance: {:?}", balances.get("alice"));
}This changes the mental model.
With a vector, you usually think in terms of position.
With a hash map, you think in terms of keys.
That is the real reason to use it.
Reading From A HashMap
get returns Option<&V> for the same reason vector get does.
The key might not exist.
use std::collections::HashMap;
fn main() {
let mut balances = HashMap::new();
balances.insert(String::from("alice"), 1_500);
match balances.get("bob") {
Some(balance) => println!("bob balance: {}", balance),
None => println!("bob was not found"),
}
}This is exactly the same pattern you learned with Option.
Collections do not introduce a new idea here.
They reuse the same type discipline.
Updating A HashMap Cleanly
The entry API is one of the most useful HashMap patterns in Rust.
use std::collections::HashMap;
fn main() {
let mut transaction_counts: HashMap<String, u64> = HashMap::new();
let wallet = String::from("alice");
*transaction_counts.entry(wallet).or_insert(0) += 1;
println!("counts: {:?}", transaction_counts);
}Why this matters:
- if the key exists, use its current value
- if the key does not exist, insert a default first
That is cleaner than writing separate "check then insert" logic.
String Versus &str
This is the other half of the lesson.
Many Rust collection problems become string problems very quickly.
The short version is:
Stringowns text&strborrows text
fn print_label(label: &str) {
println!("label: {}", label);
}
fn main() {
let owner = String::from("alice");
let network = "devnet";
print_label(&owner);
print_label(network);
}Why this works:
owneris an ownedString&ownergives a borrowed string slicenetworkis already a string slice literal
When You Need String
Use String when the program needs to own the text.
Examples:
- storing names inside structs
- inserting keys into a
HashMap<String, V> - returning text built at runtime
#[derive(Debug)]
struct Wallet {
owner: String,
}
fn build_message(owner: &str, amount: u64) -> String {
format!("{} received {} lamports", owner, amount)
}Here:
ownerinsideWalletmust be owned, so it isString- the return value of
build_messagemust also be owned, so it isString
When &str Is Better
Use &str when you only need to borrow text temporarily.
Examples:
- function parameters for read-only text input
- matching command names
- checking prefixes or suffixes
fn parse_command(input: &str) {
if input.starts_with("send") {
println!("send command");
} else if input.starts_with("balance") {
println!("balance command");
} else {
println!("unknown command");
}
}This is better than taking String because the function does not need ownership.
Parsing Text With split And trim
You will do this constantly in CLI tools and data processing.
fn main() {
let input = "send,alice,500";
let parts: Vec<&str> = input.split(',').collect();
println!("parts: {:?}", parts);
}And for cleanup:
fn main() {
let input = " alice ";
let cleaned = input.trim();
println!("cleaned: '{}'", cleaned);
}These small tools matter because messy string input is one of the fastest ways to make beginner Rust code feel confusing.
Put Collections And Strings Together
This is the pattern you will actually use.
use std::collections::HashMap;
fn main() {
let commands = vec![
"deposit alice 500",
"deposit bob 200",
"deposit alice 300",
];
let mut balances: HashMap<String, u64> = HashMap::new();
for command in commands {
let parts: Vec<&str> = command.split_whitespace().collect();
if parts.len() != 3 {
continue;
}
let name = parts[1];
let amount = match parts[2].parse::<u64>() {
Ok(value) => value,
Err(_) => continue,
};
*balances.entry(name.to_string()).or_insert(0) += amount;
}
println!("balances: {:?}", balances);
}This small example teaches a lot:
Vec<&str>from splitting inputHashMap<String, u64>for keyed storageparse::<u64>()for numeric conversionentry(...).or_insert(...)for updates
That is much closer to real Rust work than memorizing five collection types in isolation.
Common Mistakes
Mistake 1: using a vector when the real operation is keyed lookup
If the main question is "find this item by name or address," a HashMap is often the better model.
Mistake 2: taking String everywhere
If a function only reads text, prefer &str.
That makes the function easier to call and avoids unnecessary ownership transfers.
Mistake 3: indexing when get would teach better habits
Indexing is fine when you know the position exists.
When the position might be missing, get is the safer pattern.
Mistake 4: cloning strings without understanding why
Sometimes cloning is necessary.
Often it is just a sign that ownership is still unclear.
Before cloning, ask:
- do I need to own this value here
- or do I only need to borrow it
Practice
Before moving on, make sure you can do these without guessing:
- Explain when a vector is a better fit than a hash map.
- Explain why
HashMap::getreturnsOption<&V>. - Write one function that takes
&strand one that returnsString, and explain why each choice is correct. - Parse a small command string into pieces and update a
HashMap<String, u64>from it.
What You Should Know Now
After this lesson, you should be able to say:
- vectors are for ordered groups of items
- hash maps are for lookup by key
Stringowns text&strborrows text- parsing text usually means splitting, trimming, and converting carefully
That is the foundation you need for the practice page.
The next step is to use these tools in a more realistic exercise where you store, group, and search data without letting the program shape drift out of control.