Lesson 10 of 94
A First Look at String, Vec & HashMap
So far every value has had a fixed size: numbers, booleans, tuples, arrays. Real programs need text of any length, lists that grow, and lookup tables. In Rust these are String, Vec<T>, and HashMap<K, V>.
This lesson shows how to use them day to day. It deliberately avoids why some things work the way they do, such as the & in for name in &names. That "why" is ownership, and it is the whole next section. By the end of this lesson you will have seen exactly the kind of error that ownership explains.
| Concept | Rust |
|---|---|
| Growable, owned text | String |
| String literal | "literal", of type &str |
| Growable list | Vec<i32> |
| List literal | vec![1, 2, 3] |
| Add an element | v.push(x) |
| Number of elements | v.len() |
| Key-value map | HashMap<String, i32> |
| Insert and look up | map.insert(k, v) / map.get(&k) |
Part 1: String
1. Creating a String
A string literal such as "hello" has type &str: fixed text built into the program. To get a String you can own and change, convert it with String::from or .to_string().
fn main() {
let greeting = String::from("Hello");
let name = "Ferris".to_string();
println!("{greeting}, {name}!");
}Hello, Ferris!String::from calls a function that belongs to the String type. The :: means "belongs to".
2. Building a String step by step
A mut String can grow. push_str appends text and push appends one character.
fn main() {
let mut sql = String::new();
sql.push_str("SELECT id, name");
sql.push_str(" FROM users");
sql.push_str(" WHERE active = true");
sql.push(';');
println!("{sql}");
}SELECT id, name FROM users WHERE active = true;3. format! builds a new String
format! from Printing Output is the easiest way to combine values into text.
fn main() {
let first = "Ada";
let last = "Lovelace";
let year = 1815;
let bio = format!("{first} {last}, born {year}");
println!("{bio}");
}Ada Lovelace, born 18154. Length and emptiness
len() returns the length in bytes, not characters. For plain English text they are the same. Strings explains the difference for other alphabets and emoji.
fn main() {
let empty = String::new();
let word = String::from("rust");
println!("{} {}", empty.len(), empty.is_empty());
println!("{} {}", word.len(), word.is_empty());
}0 true
4 false5. Searching text
fn main() {
let url = String::from("https://example.com/api/orders?page=2");
println!("secure: {}", url.starts_with("https://"));
println!("api call: {}", url.contains("/api/"));
println!("json: {}", url.ends_with(".json"));
}secure: true
api call: true
json: false6. Changing case and trimming
These methods return new strings and leave the original unchanged.
fn main() {
let input = String::from(" Rust Is Fun ");
println!("[{}]", input.trim());
println!("[{}]", input.to_uppercase());
println!("[{}]", input.to_lowercase());
println!("[{}]", input);
}[Rust Is Fun]
[ RUST IS FUN ]
[ rust is fun ]
[ Rust Is Fun ]7. Replacing text
fn main() {
let template = String::from("Hello {name}, your order {id} has shipped");
let message = template.replace("{name}", "Sam").replace("{id}", "#8812");
println!("{message}");
}Hello Sam, your order #8812 has shipped8. Comparing strings with ==
In Rust, == compares the contents. There is no reference-equality trap.
fn main() {
let typed = String::from("secret");
let stored = "secret".to_string();
println!("same text: {}", typed == stored);
println!("matches literal: {}", typed == "secret");
}same text: true
matches literal: true9. Looping over the characters
fn main() {
let word = String::from("level");
let mut vowels = 0;
for c in word.chars() {
if c == 'a' || c == 'e' || c == 'i' || c == 'o' || c == 'u' {
vowels += 1;
}
}
println!("{word} has {vowels} vowels");
}level has 2 vowels10. Splitting into words
split_whitespace() gives the words of a text one by one, which a for loop can visit.
fn main() {
let sentence = String::from("the quick brown fox jumps");
let mut count = 0;
for word in sentence.split_whitespace() {
count += 1;
println!("{count}: {word}");
}
}1: the
2: quick
3: brown
4: fox
5: jumps11. Converting between numbers and text
to_string() turns a number into a String. Going the other way, parse() can fail, so it returns a Result, which unwrap() opens here. unwrap() means "give me the value, and panic if it failed". That is fine for a first look, and Result & the ? Operator shows how to handle failures properly.
fn main() {
let port = 8080;
let as_text = port.to_string();
println!("{} characters", as_text.len());
let input = "42";
let number: i32 = input.parse().unwrap();
println!("{}", number * 2);
}4 characters
84Part 2: Vec
12. Creating a Vec with vec!
Vec<T> is a growable list of values of type T, stored one after another in memory. The vec! macro creates one from a list of values. Print it with {:?}.
fn main() {
let primes = vec![2, 3, 5, 7, 11];
let tags = vec!["urgent", "billing"];
println!("{:?}", primes);
println!("{:?}", tags);
}[2, 3, 5, 7, 11]
["urgent", "billing"]13. Starting empty and pushing
fn main() {
let mut cart = Vec::new();
cart.push("apples");
cart.push("bread");
cart.push("milk");
println!("{:?} ({} items)", cart, cart.len());
}["apples", "bread", "milk"] (3 items)The compiler works out that cart is a Vec<&str> from what you push into it. When nothing is pushed, you must name the type yourself: let cart: Vec<&str> = Vec::new();.
14. Reading elements by index
Indexing works like arrays and panics on a bad index.
fn main() {
let queue = vec!["alice", "bob", "carol"];
println!("first: {}", queue[0]);
println!("last: {}", queue[queue.len() - 1]);
}first: alice
last: carol15. Changing elements
fn main() {
let mut scores = vec![70, 85, 90];
scores[0] = 75;
scores[2] += 5;
println!("{:?}", scores);
}[75, 85, 95]16. Removing the last element with pop
pop removes and returns the last element. The vector might be empty, so it returns an Option: Some(value) or None. Option covers it; for now, {:?} prints it.
fn main() {
let mut undo_stack = vec!["type 'a'", "type 'b'", "delete"];
println!("{:?}", undo_stack.pop());
println!("{:?}", undo_stack.pop());
println!("remaining: {:?}", undo_stack);
let mut empty: Vec<i32> = Vec::new();
println!("{:?}", empty.pop());
}Some("delete")
Some("type 'b'")
remaining: ["type 'a'"]
None17. Inserting and removing in the middle
insert(index, value) and remove(index) insert or remove an element at a position. Both shift the elements after the index.
fn main() {
let mut playlist = vec!["Intro", "Song A", "Outro"];
playlist.insert(2, "Song B");
println!("{:?}", playlist);
let removed = playlist.remove(0);
println!("removed {removed}: {:?}", playlist);
}["Intro", "Song A", "Song B", "Outro"]
removed Intro: ["Song A", "Song B", "Outro"]18. Searching and sorting
fn main() {
let mut ids = vec![42, 7, 19, 3, 25];
println!("has 19: {}", ids.contains(&19));
ids.sort();
println!("sorted: {:?}", ids);
ids.reverse();
println!("reversed: {:?}", ids);
}has 19: true
sorted: [3, 7, 19, 25, 42]
reversed: [42, 25, 19, 7, 3]The & in contains(&19) is explained in References & Borrowing.
19. Looping over a Vec
Write & in front of the vector to loop over it without giving it away, so you can still use it afterwards. The next section explains precisely what that means.
fn main() {
let temperatures = vec![21.5, 23.0, 19.8, 25.1];
let mut sum = 0.0;
for t in &temperatures {
sum += t;
}
println!("average of {} readings: {:.2}", temperatures.len(), sum / temperatures.len() as f64);
}average of 4 readings: 22.3520. Changing every element in a loop
Write &mut to loop over the elements so that you can change them. *price means "the value this points to". Both are explained in Mutable References.
fn main() {
let mut prices = vec![10.0, 25.0, 40.0];
for price in &mut prices {
*price *= 0.9; // 10% off
}
println!("{:?}", prices);
}[9.0, 22.5, 36.0]21. A Vec of Strings and join
join combines the elements into one String with a separator.
fn main() {
let mut columns = Vec::new();
columns.push(String::from("id"));
columns.push(String::from("email"));
columns.push(String::from("created_at"));
let header = columns.join(",");
println!("{header}");
}id,email,created_at22. Vectors of tuples
fn main() {
let inventory = vec![("widget", 12), ("gadget", 0), ("gizmo", 5)];
for (name, count) in &inventory {
if *count == 0 {
println!("{name}: OUT OF STOCK");
} else {
println!("{name}: {count}");
}
}
}widget: 12
gadget: OUT OF STOCK
gizmo: 5Part 3: HashMap
HashMap<K, V> maps keys to values, with fast lookup. It isn't imported automatically, so programs that use it start with use std::collections::HashMap;. This is only a first look; HashMap in the Collections section covers it in depth.
23. Creating a map and reading values
insert adds a key and its value. map["key"] reads the value for a key you know is there.
use std::collections::HashMap;
fn main() {
let mut stock = HashMap::new();
stock.insert("apples", 12);
stock.insert("pears", 0);
stock.insert("plums", 7);
println!("{} products", stock.len());
println!("apples: {}", stock["apples"]);
println!("has kiwis: {}", stock.contains_key("kiwis"));
}3 products
apples: 12
has kiwis: falseAs with Vec, the compiler works out the types: HashMap<&str, i32> here. Reading a missing key with stock["kiwis"] would stop the program with an error, like an index past the end of a Vec.
24. Looking up a key that may be missing
get is the safe lookup. Instead of returning null, it returns Some(value) or None. That's Option, Rust's replacement for null, which gets its own lesson: Option.
use std::collections::HashMap;
fn main() {
let mut prices = HashMap::new();
prices.insert(String::from("tea"), 3);
prices.insert(String::from("coffee"), 4);
println!("{:?}", prices.get("tea"));
println!("{:?}", prices.get("cocoa"));
let price = prices.get("cocoa").copied().unwrap_or(0);
println!("cocoa costs {price}");
}Some(3)
None
cocoa costs 0.copied().unwrap_or(0) means "the value if there is one, otherwise 0". Note that a HashMap<String, i32> can be searched with a plain "tea", without building a String first.
25. Updating, counting, and removing
Inserting an existing key replaces its value. For counting, entry(key).or_insert(0) gives you the value for a key, inserting 0 first if the key is new.
use std::collections::HashMap;
fn main() {
let mut scores = HashMap::new();
scores.insert("ana", 10);
scores.insert("ana", 25); // replaces 10
println!("ana: {}", scores["ana"]);
let text = "the cat and the dog and the bird";
let mut counts = HashMap::new();
for word in text.split_whitespace() {
*counts.entry(word).or_insert(0) += 1;
}
println!("the: {}, and: {}, cat: {}", counts["the"], counts["and"], counts["cat"]);
counts.remove("the");
println!("after remove: {} distinct words", counts.len());
}ana: 25
the: 3, and: 2, cat: 1
after remove: 4 distinct wordsThe * in front of counts.entry(...) means "the value this points to", exactly as in example 20.
26. Looping over a map
for (key, value) in &map visits every entry. A Rust HashMap has no predictable order, and it even changes from one run to the next. To print in a stable order, collect the keys into a Vec and sort it.
use std::collections::HashMap;
fn main() {
let mut population = HashMap::new();
population.insert("Lisbon", 545_000);
population.insert("Berlin", 3_878_000);
population.insert("Oslo", 717_000);
let mut total = 0;
for (_city, people) in &population {
total += people; // order doesn't matter for a sum
}
println!("total: {total}");
let mut cities = Vec::new();
for city in population.keys() {
cities.push(*city);
}
cities.sort();
for city in &cities {
println!("{city}: {}", population[city]);
}
}total: 5140000
Berlin: 3878000
Lisbon: 545000
Oslo: 717000When you want keys kept in sorted order all the time, use BTreeMap, covered in BTreeMap & BTreeSet.
27. Preview: why the & matters
Try looping over the vector without &, then using it again:
fn main() {
let names = vec![String::from("Ana"), String::from("Ben")];
for name in names {
println!("hello {name}");
}
println!("{} names", names.len());
}error[E0382]: borrow of moved value: `names`
--> src/main.rs:6:26
|
2 | let names = vec![String::from("Ana"), String::from("Ben")];
| ----- move occurs because `names` has type `Vec<String>`, which does not implement the `Copy` trait
3 | for name in names {
| ----- `names` moved due to this implicit call to `.into_iter()`
...
6 | println!("{} names", names.len());
| ^^^^^ value borrowed here after move
|
note: `into_iter` takes ownership of the receiver `self`, which moves `names`
--> /rustc/<version>/library/core/src/iter/traits/collect.rs:310:17
help: consider iterating over a slice of the `Vec<String>`'s content to avoid moving into the `for` loop
|
3 | for name in &names {
| +for name in names moves the vector into the loop: the loop takes ownership of it and the vector is gone afterwards. for name in &names only borrows it. This single idea, ownership, is what the whole next section is about, and it is what lets Rust manage memory safely without a garbage collector.
28. Realistic example: a to-do list
fn main() {
let mut todos: Vec<String> = Vec::new();
todos.push(String::from("write report"));
todos.push(String::from("review pull request"));
todos.push(String::from("book flights"));
let done = todos.remove(1);
println!("completed: {done}");
todos.push(format!("follow up on '{done}'"));
println!("remaining ({}):", todos.len());
for (i, task) in todos.iter().enumerate() {
println!(" {}. {task}", i + 1);
}
}completed: review pull request
remaining (3):
1. write report
2. book flights
3. follow up on 'review pull request'29. Realistic example: filtering valid email addresses
fn main() {
let raw = vec!["ana@example.com", "not-an-email", " ben@example.org ", "", "carol@test.io"];
let mut valid = Vec::new();
let mut rejected = 0;
for entry in &raw {
let email = entry.trim();
if email.contains('@') && email.contains('.') {
valid.push(email.to_lowercase());
} else {
rejected += 1;
}
}
println!("valid: {:?}", valid);
println!("rejected: {rejected}");
}valid: ["ana@example.com", "ben@example.org", "carol@test.io"]
rejected: 230. Realistic example: word statistics
fn main() {
let text = String::from("Rust is fast and Rust is safe and Rust is fun");
let mut words = Vec::new();
let mut longest = "";
for word in text.split_whitespace() {
words.push(word);
if word.len() > longest.len() {
longest = word;
}
}
let mut rust_count = 0;
for word in &words {
if *word == "Rust" {
rust_count += 1;
}
}
println!("{} words, longest: {longest}", words.len());
println!("'Rust' appears {rust_count} times");
words.sort();
words.dedup(); // remove consecutive duplicates
println!("unique words: {:?}", words);
}11 words, longest: Rust
'Rust' appears 3 times
unique words: ["Rust", "and", "fast", "fun", "is", "safe"]Summary
Stringis owned, growable text. Create it withString::from,.to_string(),String::new(), orformat!.==compares string contents. There is noequalstrap.Vec<T>is a growable list. Create it withvec![...]orVec::new(), then usepush,pop,insert,remove,len,contains, andsort.- Loop with
for x in &vto read andfor x in &mut vto change, and the vector stays usable afterwards. HashMap<K, V>maps keys to values. Useinsert,get(which returnsSome(value)orNone),contains_key,remove, andentry(k).or_insert(0)for counting. Its iteration order is unpredictable, so sort the keys when order matters.- Without
&, the loop takes the vector away. The next section explains why.
Next: the core of Rust, Ownership.
