Collections

Vec growth, HashMap and HashSet, String versus str, and Cow plus Path when ownership of text is optional.

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.

Vecptr, len, cape0 e1 e2heap buffer
StatusLength is how many elements you have

Capacity is how many fit before a new allocation. push writes into spare capacity when there is room.

OperationWhat 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, removeAdd or take an element. After insert or remove, later indexes have shifted.
len, is_empty, clearLength, emptiness, and drop every element
retain, extend, drain, sortKeep 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 &vBorrows each element
for x in &mut vBorrows each element mutably
for x in vConsumes the vector and yields owned elements
fn takes a sliceAccepts 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.

01_vec.rsRust
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.

"ada"keyhashbucketada → 1stored pair
StatusThe key picks a bucket

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.

MethodWhat it does
insert, get, get_mut, remove, contains_keyThe basic map operations
entry(k).or_insert(0)Inserts only when the key is missing, then you can add to that value
or_insert_withRuns the expensive default only on a miss
and_modifyChanges an existing value and can still or_insert when the key is new
HashSet insert, contains, removeA membership test. Nothing is stored except the key.
BTreeMapKeys stay sorted when you need ranges. A HashMap does not keep insertion order.
02_hashmap_and_hashset.rsRust
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.

PlaceType
A function parameterA string slice, so a literal and a String both work
A struct field that owns textString
A return value that is new textString
A return value that views an inputA slice. It cannot outlive that input.
Stringptr, len, cap&strptr, lenUTF-8bytes
StatusThe String owns the buffer

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.

03_string_vs_str.rsRust
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.

input&strBorrowedno allocationOwnednew String
StatusNo change, no allocation

Parsers and normalizers return the original slice when it is already valid.

TypeSame idea asNote
PathA string sliceA view. Functions take something that can be viewed as a path.
PathBufStringThe owned path
OsStringBytes that might not be UTF-8Environment 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.

04_cow_path_osstring.rsRust
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());
}