Rust Strings, Vectors and Hash Maps¶
Rust String, Vec and HashMap hold data that can grow at runtime. This lesson extends our Raspberry Pi fixtures to a collection of readings and labelled values, while keeping ownership, missing data and UTF-8 boundaries explicit.
Prerequisites and outcome¶
Complete enums, match and patterns. You will distinguish owned String from borrowed str, use a vector rather than a fixed-size array, and perform map lookups without assuming an entry exists.
These collection types are standard-library APIs, not new syntax for defining types. Type parameters such as Vec<i32> name the element type; HashMap<String, i32> names the key and value types. Generics receive their own lesson later. Vec and String are in the prelude; HashMap needs an explicit import.
Run a collection report¶
Replace src/main.rs with:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 | |
Expected output:
Some and None are variants of the standard-library Option enum. Here we inspect them with Debug formatting or match; lesson 9 develops absence and error handling in detail. Some(&0) in the map test is different from None: a real zero is present.
Vec owns elements; slices borrow them¶
An array's length is part of its type. A Vec keeps a runtime length and can add or remove elements. vec![...] is a macro for constructing a vector; Vec::new() starts empty. push requires mutable access. With non-Copy elements, insertion can move ownership into the vector.
len() counts initialized elements; capacity() describes how many elements the current allocation can hold without reallocation. Growing beyond capacity can change the backing allocation. Do not keep a reference to an element while pushing into that same vector and expect borrow checking to permit it.
Use &Vec<T> or, more generally for read-only element access, a slice &[T]. Our helper needs &mut Vec<i32> specifically because a mutable slice cannot grow its backing collection. Iterating over &readings borrows elements; consuming readings by value in a loop transfers access to owned elements and consumes the vector.
readings[index] panics when the runtime index is invalid. get(index) returns an Option and lets the caller decide what absence means. The expected-panic test demonstrates the difference, not an error-handling strategy for production. See vectors and the Vec API.
String owns UTF-8 text; str is a text view¶
String is growable owned UTF-8 text, while &str borrows a view. push_str appends borrowed text without taking ownership of that source. push appends a char. Calling as_str() borrows a String as text; to_owned() on a str creates owned text. A string literal already provides a borrowed view and does not need to become a String just for inspection.
Length and indexing are byte-based. chars() iterates Unicode scalar values, not user-perceived characters. The combining-sequence test shows that one displayed character may comprise two scalars. Grapheme segmentation is a separate operation that the standard chars iterator does not provide.
String does not support an integer index as “the character at this position”. Valid byte-range slicing must align with UTF-8 boundaries. get(..1) on π returns None instead of panicking, unlike direct slicing in the previous lesson. Neither byte counts nor scalar counts are a universal display-width measurement.
For concatenation, left + &right consumes the left String, while format!("{left}{right}") borrows arguments and produces new owned text. Pick an operation for its ownership contract, not because all concatenation spellings look interchangeable. See strings and the String API.
HashMap associates keys with values¶
HashMap owns the keys and values inserted by value. Our String label moves into the map, but the i32 reading copies. A String-keyed map can be looked up with a borrowed str, so finding "Pi 4B" does not require allocating a new String key.
get returns a borrowed value if present. insert returns an owned previous value when replacing the same key; remove returns the removed value. Their Option results make absence explicit. To initialise or update an entry in one operation, the entry API provides operations such as entry(key).or_insert(default), returning mutable access to the stored value. Its key is owned, unlike a borrowed get lookup.
Iteration order is not a stable reporting contract. We intentionally report a known sequence of names instead of printing the map directly. A different data structure, BTreeMap, provides key-ordered iteration when that ordering is part of the requirement.
Read hash maps and the HashMap API. We postpone trait requirements for custom keys until traits and generics.
Deliberately failing reference across growth¶
Use a separate scratch project for this complete example:
Expect E0502 from cargo check. Using the reference before pushing ends that particular overlapping use; alternatively, copy an i32 value when you actually need an independent value. A String element would require a different ownership decision. Reserved capacity alone is not permission to retain a conflicting reference across push.
Exercises and checks¶
- Replace
&readings[0]in the failure example withreadings[0]: it is now a copied i32, so the program can print it after push. Expect 46700. - Add
latest.insert(String::from("Pi 4B"), 0)before reporting: expect a present zero rather than a missing entry. The map replacement test covers the underlying rule. - Compare
"é"with"e\u{301}": different byte/scalar sequences can have similar visual appearance. Do not assume Rust automatically normalises them or treats them as equal keys. - Try using
labelafter inserting it into latest: expect a moved-value compile error. Restore the program, or borrow a key from the map when inspection is what you need.
Verification and next step¶
On October 10, 2026, this lesson was verified on a Raspberry Pi 4B with 64-bit user space, kernel 6.18.50+rpt-rpi-v8, Rust and Cargo 1.99.0, and edition 2024. Cargo check, all 8 tests, formatting and debug/release output comparisons passed. The copied-element and present-zero exercises produced the expected output; reference-across-push and moved-label examples were rejected with E0502 and E0382. No hardware access, map-order assumption or performance result is involved.
Continue with Option, Result and error handling, including the Part I checkpoint.