10  Ownership

Objective

Understand the how Rust ensures memory safety through the borrow checker and be able to avoid issues by borrowing, using a slice, or cloning.

Rust is notorious for its borrow checker. The borrow check is Rust’s secret sauce which helps ensure memory safety. At compile time, Rust looks through all of your code to enforce ownership. The key is that

variables can be used only once!

If a variable has been used it has been moved. There are a number of ways to reuse variables.

Moves

A move occurs whenever a variable is used by a function or a method.

fn main() {
    let x = vec![1.0, 2.0, 3.0];
    let y = x; // ⬅️ ownership moved
    println!("{:?}", x); // ❌ error: value used after move
}

Since x was moved (used) by assigning it to y, the original value cannot be used.

Cloning

The simplest but least efficient way to reuse a variable is to clone it. This is what R does.

Remember: when in doubt, clone it out!

Almost everything in Rust can be cloned by using the .clone() method.

Cannot compile
let x  = v![0.0, 3.14, 10.1, 44.8];

let avg1 = mean(x); // ⬅️ x moved here!
let avg2 = mean(x); // ❌ compiler error

If we first clone x before using mean() we can use x again.

let x  = v![0.0, 3.14, 10.1, 44.8];

let avg1 = mean(x.clone()); // ⬅️ x cloned here!
let avg2 = mean(x); // ✅ compiler happy

Borrowing

While variables may only be used once, they can be borrowed infinitely (until they go out of scope). A variable can be used by reference when the & symbol is placed in front of it—e.g. arg: &Vec<f64>.

fn main() {
    let x = vec![1.0, 2.0, 3.0];
    let avg = mean(&x); // 👈 borrowing `x`
    println!("x is still usable: {:?}", x);
}

If you borrow a variable you cannot mutate it or move it.

Slices

Slices are a special type of borrowing. Slices are a reference to contiguous section of the same type. They’re recognized by the syntax &[T].

Slices always have a known length (accessed via .len()) and can be used from more than one type.

Example

Both an array [f64; N] and a Vec<f64> can be turned into a &[f64]

let x = [0.0, 20.0, 742.3];
let y = vec![1.0, 2.0, 3.0];

let avg_x = mean(&x)
let avg_y = mean(&y);

Slices are more flexible and more light-weight than borrowing a full vector and should be preferred whenever possible.

In general: slice > reference > owned

Exercise 1

  • Calculate the mean of the same Vec<f64> twice
  • Print the vector at the end
View solution
fn mean(x: Vec<f64>) -> f64 {
    let mut total = 0.0;
    for xi in x {
        total += xi;
    }
    total / x.len() as f64
}

fn main() {
    let x = vec![1.0, 2.0, 3.0, 4.0, 5.0];
    let result = mean(x.clone());
    let result2 = mean(x.clone())
    println!("The vector is still {:?}", x);
}

Exercise 2

  • Rewrite the mean() function to accept a reference to a Vec<f64> instead of taking ownership
  • Then call it with a borrowed vector.
View solution
fn mean(x: &Vec<f64>) -> f64 {
    let mut total = 0.0;
    for xi in x {
        total += xi;
    }
    total / x.len() as f64
}

fn main() {
    let x = vec![1.0, 2.0, 3.0, 4.0, 5.0];
    let result = mean(&x);
    println!("The mean of x is {result}");
}

Exercise 3

  • Rewrite mean() to accept a slice (&[f64]) instead of a Vec<f64> reference.
  • Create an array of f64 values called y
  • Calculate the mean on both x and y
  • Print both averages
View solution
fn mean(x: &[f64]) -> f64 {
    let mut total = 0.0;
    for xi in x {
        total += xi;
    }
    total / x.len() as f64
}

fn main() {
    let x = vec![1.0, 2.0, 3.0, 4.0, 5.0];
    let y = [0.0, 9.5, 3.3, 11.78, 3.14159];
    println!("The mean of x is {}.\nThe mean of y is {}", mean(&x), mean(&y));
}