Kyle Edwards

Rust Basics

Numeric Scalars

Collections

Tuples, structs, and arrays are zero overhead data structures. The compiler keeps track of element index positions in memory. They are also stack-allocated, unlike vectors which are heap-allocated.

Vectors vs. Arrays

let mut nums: [u8; 3] = [1, 2, 3];
let mut nums: Vec<u8> = vec![1, 2, 3];

The two main differences are that vectors are dynamic in size, and they are stored on the heap rather than the stack. Because all stack items must have a fixed size, the Vec<T> return value is in fact a struct called VecMetadata with a memory address for the first element, the vector’s length, and its capacity.

Vec::with_capacity(n) initializes a vector in the heap with a reserved amount of free memory, which can be used to avoid the need for a lot of unnecessary reallocation.

Built-in Enums

Both Option and Result are built-in enums that are commonly used when accessing dynamic data that may or may not be reliable. Rust requires that all variants of a type must be handled when pattern matching, which alleviates the need for a true null value in the language and leads to better safety.

enum Option<T> {
    None,
    Some(T),
}

None => Option::None
Some(T) => Option::Some<T>

enum Result<O, E> {
    Ok(O),
    Err(E),
}

Ownership

Unlike languages that require manual memory management like C or garbage-collected languages like JavaScript, Rust manages allocating and deallocating memory with a set of rules around ownership, borrowing, and lifetimes.

By having enforcing these rules, classes of memory bugs like use-after-free errors, double-free errors, and memory leaks are eliminated while still providing the performance benefits of a non-GCed systems language.

Note: Of course, perfectly written C code would also be free of these bugs, but Rust can provide some peace of mind knowing that programs should not be open to memory-related security holes.

Heap-allocated values are freed when they go out of scope and there are no other references to the object. Function arguments and return values allow this data to be moved between scopes. You can also provide hinting to the compiler that memory can be freed if you use a block expression like so:

fn scope() {
    let x: Vec<u8> = vec![1, 2, 3];
    println!("The second number is {}!", x[1]);

    { // A
        let y: Vec<u8> = vec![4, 5, 6];
        println!("The third number is {}!", y[2]);
    } // `y` is freed here.

    // `x` is not freed until here.
}

Note: Because block expressions are well… expressions, they can be used as value assignments like let x: i32 = { do_something(); 5 };.

Cloning

Because passing data into a function call transfers ownership, unless you use borrowing (see the next section) or return the original value back out to the original scope, the only other way to get around this is with cloning. You can use the .clone() method to duplicate the value before it’s passed into a function call, like let newVector: Vec<u8> = reverse_vector(vector.clone());.

This can be relatively slow to do a full deep copy of the object, so it’s important to not use cloning as a crutch if you’re not totally comfortable with the ownership system.

References and Borrowing

References are a way to retain ownership but allow called functions to “borrow” them. Unlike cloning, this does not copy any data in memory, but still allows you to pass data around without having to worry about the transfer of ownership.

By default, references are immutable, but it is possible to create mutable references with &mut. Vec<T>.clear() is a method that takes a mutable reference to &mut self that truncates a vector.

Mutable references have an additional restriction where there any be no other references currently in scope. There can be as many immutable references at the same time as you like. The reason for this is to prevent race conditions in concurrent programs.

Slices

Slices are essentially references into an existing piece of data on the heap. Like the VecMetadata stack-allocated data, there’s also SliceMetadata that holds the reference to the first element’s heap index and the length of the slice.

Slices have their own types like &str or &[u32].

Types can have a trait that allows a borrowed reference to be coerced into a slice.

Lifetimes

Lifetime annotations help keep track of when variables and struct properties go out of scope. It can be a bit confusing at first, but they do not make any attempt to alter the lifetime of a variable.

Lifetime annotations are required when references are used as struct properties, to inform the compiler these fields will stick around for the life of the struct. Without these, you could incur a use-after-free error once the variable goes out of scope.

Other Topics

Resources