Guide contents

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().

Rust
fn main() {
    let greeting = String::from("Hello");
    let name = "Ferris".to_string();
    println!("{greeting}, {name}!");
}
Output
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.

Rust
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}");
}
Output
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.

Rust
fn main() {
    let first = "Ada";
    let last = "Lovelace";
    let year = 1815;
    let bio = format!("{first} {last}, born {year}");
    println!("{bio}");
}
Output
Ada Lovelace, born 1815

4. 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.

Rust
fn main() {
    let empty = String::new();
    let word = String::from("rust");
    println!("{} {}", empty.len(), empty.is_empty());
    println!("{} {}", word.len(), word.is_empty());
}
Output
0 true
4 false

5. Searching text

Rust
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"));
}
Output
secure: true
api call: true
json: false

6. Changing case and trimming

These methods return new strings and leave the original unchanged.

Rust
fn main() {
    let input = String::from("  Rust Is Fun  ");
    println!("[{}]", input.trim());
    println!("[{}]", input.to_uppercase());
    println!("[{}]", input.to_lowercase());
    println!("[{}]", input);
}
Output
[Rust Is Fun]
[  RUST IS FUN  ]
[  rust is fun  ]
[  Rust Is Fun  ]

7. Replacing text

Rust
fn main() {
    let template = String::from("Hello {name}, your order {id} has shipped");
    let message = template.replace("{name}", "Sam").replace("{id}", "#8812");
    println!("{message}");
}
Output
Hello Sam, your order #8812 has shipped

8. Comparing strings with ==

In Rust, == compares the contents. There is no reference-equality trap.

Rust
fn main() {
    let typed = String::from("secret");
    let stored = "secret".to_string();
    println!("same text: {}", typed == stored);
    println!("matches literal: {}", typed == "secret");
}
Output
same text: true
matches literal: true

9. Looping over the characters

Rust
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");
}
Output
level has 2 vowels

10. Splitting into words

split_whitespace() gives the words of a text one by one, which a for loop can visit.

Rust
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}");
    }
}
Output
1: the
2: quick
3: brown
4: fox
5: jumps

11. 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.

Rust
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);
}
Output
4 characters
84

Part 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 {:?}.

Rust
fn main() {
    let primes = vec![2, 3, 5, 7, 11];
    let tags = vec!["urgent", "billing"];
    println!("{:?}", primes);
    println!("{:?}", tags);
}
Output
[2, 3, 5, 7, 11]
["urgent", "billing"]

13. Starting empty and pushing

Rust
fn main() {
    let mut cart = Vec::new();
    cart.push("apples");
    cart.push("bread");
    cart.push("milk");
    println!("{:?} ({} items)", cart, cart.len());
}
Output
["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.

Rust
fn main() {
    let queue = vec!["alice", "bob", "carol"];
    println!("first: {}", queue[0]);
    println!("last: {}", queue[queue.len() - 1]);
}
Output
first: alice
last: carol

15. Changing elements

Rust
fn main() {
    let mut scores = vec![70, 85, 90];
    scores[0] = 75;
    scores[2] += 5;
    println!("{:?}", scores);
}
Output
[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.

Rust
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());
}
Output
Some("delete")
Some("type 'b'")
remaining: ["type 'a'"]
None

17. 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.

Rust
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);
}
Output
["Intro", "Song A", "Song B", "Outro"]
removed Intro: ["Song A", "Song B", "Outro"]

18. Searching and sorting

Rust
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);
}
Output
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.

Rust
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);
}
Output
average of 4 readings: 22.35

20. 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.

Rust
fn main() {
    let mut prices = vec![10.0, 25.0, 40.0];
    for price in &mut prices {
        *price *= 0.9; // 10% off
    }
    println!("{:?}", prices);
}
Output
[9.0, 22.5, 36.0]

21. A Vec of Strings and join

join combines the elements into one String with a separator.

Rust
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}");
}
Output
id,email,created_at

22. Vectors of tuples

Rust
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}");
        }
    }
}
Output
widget: 12
gadget: OUT OF STOCK
gizmo: 5

Part 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.

Rust
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"));
}
Output
3 products
apples: 12
has kiwis: false

As 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.

Rust
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}");
}
Output
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.

Rust
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());
}
Output
ana: 25
the: 3, and: 2, cat: 1
after remove: 4 distinct words

The * 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.

Rust
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]);
    }
}
Output
total: 5140000
Berlin: 3878000
Lisbon: 545000
Oslo: 717000

When 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:

Rust✗ does not compile · E0382
fn main() {
    let names = vec![String::from("Ana"), String::from("Ben")];
    for name in names {
        println!("hello {name}");
    }
    println!("{} names", names.len());
}
Output
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

Rust
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);
    }
}
Output
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

Rust
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}");
}
Output
valid: ["ana@example.com", "ben@example.org", "carol@test.io"]
rejected: 2

30. Realistic example: word statistics

Rust
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);
}
Output
11 words, longest: Rust
'Rust' appears 3 times
unique words: ["Rust", "and", "fast", "fun", "is", "safe"]

Summary

  • String is owned, growable text. Create it with String::from, .to_string(), String::new(), or format!.
  • == compares string contents. There is no equals trap.
  • Vec<T> is a growable list. Create it with vec![...] or Vec::new(), then use push, pop, insert, remove, len, contains, and sort.
  • Loop with for x in &v to read and for x in &mut v to change, and the vector stays usable afterwards.
  • HashMap<K, V> maps keys to values. Use insert, get (which returns Some(value) or None), contains_key, remove, and entry(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.