Collections
Arrays were fixed. Slices were views. This lesson adds the growable list, then a map, then the text types you already borrowed, then the path types that are the same idea for the filesystem. Iterators that chain map and filter wait until after closures.
Vec
Vec is a contiguous growable array on the heap. It is the default list. On the stack you still have three words: pointer, length, and capacity. The elements sit in the heap buffer. capacity may be larger than length.
Capacity is how many fit before a new allocation. push writes into spare capacity when there is room.
| Operation | What it does |
|---|---|
Vec::new, vec![1, 2, 3] | Create an empty vector, or one from values |
Vec::with_capacity(n) | Create one that can hold n elements without reallocating. Use this when you know the size. |
| push, pop, insert, remove | Add or take an element. After insert or remove, later indexes have shifted. |
| len, is_empty, clear | Length, emptiness, and drop every element |
| retain, extend, drain, sort | Keep some, append another iterator, take a range, or order the elements |
| get(i) | An Option. A missing index is a value. |
| v[i] | Panics if the index is out of bounds. Use it only after you have proved the index. |
| for x in &v | Borrows each element |
for x in &mut v | Borrows each element mutably |
| for x in v | Consumes the vector and yields owned elements |
| fn takes a slice | Accepts a Vec, because the vector can lend its buffer |
Prefer Vec over a linked list. After insert or remove, later indexes have shifted. Shared ownership of the items is a later lesson. Do not clone a large vector to avoid a borrow.
fn sum(xs: &[i32]) -> i32 {
xs.iter().copied().sum()
}
fn main() {
let mut nums = Vec::with_capacity(4);
nums.push(10);
nums.push(20);
nums.push(30);
println!("{} {}", nums.get(1).copied().unwrap_or(0), sum(&nums));
}HashMap and HashSet
A HashMap stores a value under a key. A HashSet stores unique keys and nothing else. Average lookup and insert are constant time. The key type must be comparable for equality and hashable. String, u64, and a newtype that derives those behaviors are normal keys. Do not use f64 as a key. NaN does not behave as a key.
insert and get hash the key. A HashMap of String can be queried with a string slice, so you do not allocate a new String just to look up.
| Method | What it does |
|---|---|
| insert, get, get_mut, remove, contains_key | The basic map operations |
| entry(k).or_insert(0) | Inserts only when the key is missing, then you can add to that value |
| or_insert_with | Runs the expensive default only on a miss |
| and_modify | Changes an existing value and can still or_insert when the key is new |
HashSet insert, contains, remove | A membership test. Nothing is stored except the key. |
BTreeMap | Keys stay sorted when you need ranges. A HashMap does not keep insertion order. |
use std::collections::{HashMap, HashSet};
fn main() {
let mut scores = HashMap::new();
scores.insert(String::from("ada"), 10);
*scores.entry(String::from("ada")).or_insert(0) += 5;
let ada = scores.get("ada").copied().unwrap_or(0);
let mut tags = HashSet::new();
tags.insert("rust");
println!("{ada} {}", tags.contains("rust"));
}String and string slices
String owns growable UTF-8 on the heap: pointer, length, and capacity. A string slice does not own the bytes: pointer and length only. You used both while borrowing.
| Place | Type |
|---|---|
| A function parameter | A string slice, so a literal and a String both work |
| A struct field that owns text | String |
| A return value that is new text | String |
| A return value that views an input | A slice. It cannot outlive that input. |
String::from, to_string, and format! create one. push_str and push grow it. clear empties it.
Filesystem paths and some environment values are not guaranteed to be UTF-8. Those use a path or an OsString, which is the next section, not a String.
fn shout(name: &str) -> String {
format!("{name}!")
}
fn main() {
let owned = String::from("résumé");
let view: &str = &owned;
let mut chars = 0;
for _ in view.chars() {
chars += 1;
}
println!("{} {chars}", shout(view));
}Cow, Path, and OsString
Cow is an enum with two variants: borrowed, or owned. For text, that is either a string slice or a String. If the input needs no change, return the borrow and allocate nothing. If it needs a change, allocate once and return the owned string. The borrowed form cannot outlive the input. Lifetime syntax is the next idea after generics. The choice itself is borrow or own, which you already know.
Parsers and normalizers return the original slice when it is already valid.
| Type | Same idea as | Note |
|---|---|---|
Path | A string slice | A view. Functions take something that can be viewed as a path. |
PathBuf | String | The owned path |
OsString | Bytes that might not be UTF-8 | Environment values and arguments. to_string_lossy is for display, not for storing the real value. |
Do not turn a Path into a String as a habit. That conversion drops information on some operating systems.
use std::borrow::Cow;
use std::path::{Path, PathBuf};
fn normalize(input: &str) -> Cow<'_, str> {
if input.contains(' ') {
Cow::Owned(input.replace(' ', "_"))
} else {
Cow::Borrowed(input)
}
}
fn join_logs(dir: &Path) -> PathBuf {
dir.join("app.log")
}
fn main() {
println!("{}", normalize("a b"));
println!("{}", join_logs(Path::new("/var")).display());
}