10 Ownership
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.
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 {
+= xi;
total }
/ x.len() as f64
total }
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 aVec<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 {
+= xi;
total }
/ x.len() as f64
total }
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 aVec<f64>
reference. - Create an array of
f64
values calledy
- Calculate the mean on both
x
andy
- Print both averages
View solution
fn mean(x: &[f64]) -> f64 {
let mut total = 0.0;
for xi in x {
+= xi;
total }
/ x.len() as f64
total }
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));
}