Collections

Using Arrays

Arrays ([T; N]) have a fixed size.

fn main() {
    let array = [1, 2, 3, 4, 5];
    println!("array = {:?}", array);
}

%3array:array:array12345

Building the array at runtime.

How do you know how many 'slots' you've used?

fn main() {
    let mut array = [0u8; 10];
    for idx in 0..5 {
        array[idx] = idx as u8;
    }
    println!("array = {:?}", array);
}

%3array:array:array0123400000

Slices

A view into some other data. Written as &[T] (or &mut [T]).

fn main() {
    let mut array = [0u8; 10];
    for idx in 0..5 {
        array[idx] = idx as u8;
    }
    let data = &array[0..5];
    println!("data = {:?}", data);
}

%3data:data:array:array:data:->array:array0123400000dataptrlen = 5data:p0->array:f0

Note: Slices are unsized types and can only be access via a reference. This reference is a 'fat reference' because instead of just containing a pointer to the start of the data, it also contains a length value.

Vectors

Vec is a growable, heap-allocated, array-like type.

fn process_data(input: &[u32]) {
    let mut vector = Vec::new();
    for value in input {
        vector.push(value * 2);
    }
    println!("vector = {:?}, first = {}", vector, vector[0]);
}

fn main() { process_data(&[1, 2, 3]); }

%3vector:vector:_inner2460vectorptrlen = 3cap = 4vector:p0->_inner:f0

Note:

The green block of data is heap allocated.

There's a macro short-cut too...

fn main() {
    let mut vector = vec![1, 2, 3, 4];
}

Check out the docs!

Features of Vec

  • Growable (will re-allocate if needed)
  • Can borrow it as a &[T] slice
  • Can access any element (vector[i]) quickly
  • Can push/pop from the back easily

Downsides of Vec

  • Not great for insertion
  • Everything must be of the same type
  • Indices are always usize

String Slices

The basic string types in Rust are all UTF-8.

A String Slice (&str) is an immutable view on to some valid UTF-8 bytes

fn main() {
    let bytes = [0xC2, 0xA3, 0x39, 0x39, 0x21];
    let s = std::str::from_utf8(&bytes).unwrap();
    println!("{}", s);
}

%3s:s:bytes:bytes:s:->bytes:bytes0xC20xA30x390x390x21sptrlen = 5s:p0->bytes:f0

Note:

A string slice is tied to the lifetime of the data that it refers to.

String Literals

  • String Literals produce a string slice "with static lifetime"
  • Points at some bytes that live in read-only memory with your code
fn main() {
    let s: &'static str = "Hello!";
    println!("s = {}", s);
}

%3s:s:bytes0x480x650x6c0x6c0x6f0x21sptrlen = 5s:p0->bytes:f0

Note:

The lifetime annotation of 'static just means the string slice lives forever and never gets destroyed. We wrote out the type in full so you can see it - you can emit it on variable declarations.

There's a second string literal in this program. Can you spot it?

(It's the format string in the call to println!)

Strings (docs)

  • A growable collection of char
  • Actually stored as a Vec<u8>, with UTF-8 encoding
  • You cannot access characters by index (only bytes)
    • But you never really want to anyway
fn main() {
    let string = String::from("Hello!");
}

%3string:string:_inner0x480x650x6c0x6c0x6f0x21stringptrlen = 6cap = 6string:p0->_inner:f0

Note:

The green block of data is heap allocated.

Making a String

fn main() {
    let s1 = "String literal up-conversion".to_string();
    let s2: String = "Into also works".into();
    let s3 = String::from("Or using from");
    let s4 = format!("String s1 is {:?}", s1);
    let s5 = String::new(); // empty
}

Appending to a String

fn main() {
    let mut start = "Mary had a ".to_string();
    start.push_str("little");
    let rhyme = start + " lamb";
    println!("rhyme = {}", rhyme);
    // println!("start = {}", start);
}

Joining pieces of String

fn main() {
    let pieces = ["Mary", "had", "a", "little", "lamb"];
    let rhyme = pieces.join(" ");
    println!("Rhyme = {}", rhyme);
}

VecDeque (docs)

A ring-buffer, also known as a Double-Ended Queue:

use std::collections::VecDeque;
fn main() {
    let mut queue = VecDeque::new();
    queue.push_back(1);
    queue.push_back(2);
    queue.push_back(3);
    println!("first: {:?}", queue.pop_front());
    println!("second: {:?}", queue.pop_front());
    println!("third: {:?}", queue.pop_front());
}

Features of VecDeque

  • Growable (will re-allocate if needed)
  • Can access any element (queue[i]) quickly
  • Can push/pop from the front or back easily

Downsides of VecDeque

  • Cannot borrow it as a single &[T] slice without moving items around
  • Not great for insertion in the middle
  • Everything must be of the same type
  • Indices are always usize

HashMap (docs)

If you want to store Values against Keys, Rust has HashMap<K, V>.

Note that the keys must be all the same type, and the values must be all the same type.

use std::collections::HashMap;
fn main() {
    let mut map = HashMap::new();
    map.insert("Triangle", 3);
    map.insert("Square", 4);
    println!("Triangles have {:?} sides", map.get("Triangle"));
    println!("Triangles have {:?} sides", map["Triangle"]);
    println!("map {:?}", map);
}

Note: The index operation will panic if the key is not found, just like with slices and arrays if the index is out of bounds. Get returns an Option.

If you run it a few times, the result will change because it is un-ordered.

The Entry API

What if you want to update an existing value OR add a new value if it's not there yet?

HashMap has the Entry API:

enum Entry<K, V> {
    Occupied(...),
    Vacant(...),
}

fn entry(&mut self, key: K) -> Entry<K, V> {
    ...
}

Entry API Example

use std::collections::HashMap;
 
fn update_connection(map: &mut HashMap<i32, u64>, id: i32) {
    map.entry(id)
        .and_modify(|v| *v = *v + 1)
        .or_insert(1);
}
 
fn main() {
    let mut map = HashMap::new();
    update_connection(&mut map, 100);
    update_connection(&mut map, 200);
    update_connection(&mut map, 100);
    println!("{:?}", map);
}

Features of HashMap

  • Growable (will re-allocate if needed)
  • Can access any element (map[i]) quickly
  • Great at insertion
  • Can choose the Key and Value types independently

Downsides of HashMap

  • Cannot borrow it as a single &[T] slice
  • Everything must be of the same type
  • Unordered

BTreeMap (docs)

Like a HashMap, but kept in-order.

use std::collections::BTreeMap;
fn main() {
    let mut map = BTreeMap::new();
    map.insert("Triangle", 3);
    map.insert("Square", 4);
    println!("Triangles have {:?} sides", map.get("Triangle"));
    println!("Triangles have {:?} sides", map["Triangle"]);
    println!("map {:?}", map);
}

Features of BTreeMap

  • Growable (will re-allocate if needed)
  • Can access any element (map[i]) quickly
  • Great at insertion
  • Can choose the Key and Value types independently
  • Ordered

Downsides of BTreeMap

  • Cannot borrow it as a single &[T] slice
  • Everything must be of the same type
  • Slower than a HashMap

Sets

We also have HashSet and BTreeSet.

Just sets the V type parameter to ()!


TypeOwnsGrowIndexSliceCheap Insert
Arrayusize
Sliceusize
Vecusize
String Slice🤔
String🤔
VecDequeusize🤔↪ / ↩
HashMapT
BTreeMapT

Note:

The 🤔 for indexing string slices and Strings is because the index is a byte offset and the system will panic if you try and chop a UTF-8 encoded character in half.

The 🤔 for indexing VecDeque is because you might have to get the contents in two pieces (i.e. as two disjoint slices) due to wrap-around.

Technically you can insert into the middle of a Vec or a String, but we're talking about 'cheap' insertions that don't involve moving too much stuff around.