Lesson Details

Data is owned by one variable

In Rust, data has a single owner. This means a single variable is responsible for the lifecycle of a specific piece of data. In this piece of code, the numbers variable owns the Vec data. A Vec is a list of data that be added to or removed from.

fn main() {
let numbers = vec![10, 20, 30];
prints_numbers(numbers);
}

fn prints_numbers(vec_of_numbers: Vec<i32>) {
dbg!(vec_of_numbers);
}

When using the Vec data in our application we can either share references to it, which retains ownership of the data in numbers, or we can move it to a new owner.

The prints_numbers function takes ownership of the data, so in this case we move the data. This means that calling prints_numbers twice would fail to compile because the first prints_numbers moves the data out of the numbers variable, into the vec_of_numbers variable, which means it’s no longer in numbers for the second prints_numbers call to access.

This code will result in a compile error.

fn main() {
let numbers = vec![10, 20, 30];
prints_numbers(numbers);
prints_numbers(numbers);
}

The compiler warns us about the program having moved the data, while also providing suggestions for how to fix that.

❯ cargo run
Compiling quickstart v0.1.0 (/quickstart)
error[E0382]: use of moved value: `numbers`
--> src/main.rs:4:20
|
2 | let numbers = vec![10, 20, 30];
| ------- move occurs because `numbers` has type `Vec<i32>`, which does not implement the `Copy` trait
3 | prints_numbers(numbers);
| ------- value moved here
4 | prints_numbers(numbers);
| ^^^^^^^ value used here after move
|
note: consider changing this parameter type in function `prints_numbers` to borrow instead if owning the value isn't necessary
--> src/main.rs:7:35
|
7 | fn prints_numbers(vec_of_numbers: Vec<i32>) {
| -------------- ^^^^^^^^ this parameter takes ownership of the value
| |
| in this function
help: consider cloning the value if the performance cost is acceptable
|
3 | prints_numbers(numbers.clone());
| ++++++++

In this way, Rust will, at compile time, guarantee that your data isn’t changing out from under you while operating on it because the compiler knows which functions are taking ownership, which are mutating it, and which only have read-only access. Ownership turns this problem into something the compiler can check for you instead of something you need to keep track of in your head while programming or doing large refactors.

This yields huge benefits for teams which can merge PRs knowing that one person’s merge won’t silently invalidate another person’s data usage in their PR. The the compiler will keep track of the issues for you.

Working with Ownership

So when it comes to our example that currently has a compile error we can fix it in one of three ways:

  • clone the data, creating a second duplicate value with a different owner
  • moving the data back out of the function by returning it
  • share a reference, which allows a function to temporarily access the data in read-only or mutable, compiler-checked, access patterns

Cloning

Cloning the data allows us to pass a duplicate value into the first function, while moving the value in numbers into the second function.

fn main() {
let numbers = vec![10, 20, 30];
prints_numbers(numbers.clone());
prints_numbers(numbers);
}

fn prints_numbers(vec_of_numbers: Vec<i32>) {
dbg!(vec_of_numbers);
}

This is a good option if the values are lightweight or if multiple copies are needed.

Moving data

We can also continue moving the data once we’ve moved it into the function… by moving it back out! The prints_numbers function can take ownership of the Vec<i32> and then return it as the return value.

The dbg! macro also takes ownership of the Vec<i32>, and returns the value it took ownership of. So remembering that Rust is primarily expression-based, the value returned by the dbg! macro is in turn, returned from the function.

Since we’re moving out of numbers, we have to create a new variable to own the return value of prints_numbers. This new_numbers variable is what we’ll pass into the second function, moving the data again.

fn main() {
let numbers = vec![10, 20, 30];
let new_numbers = prints_numbers(numbers);
prints_numbers(new_numbers);
}

fn prints_numbers(vec_of_numbers: Vec<i32>) -> Vec<i32> {
dbg!(vec_of_numbers)
}

References

Passing a reference provides temporary read-only (or read/write) access to the value in numbers, while numbers retains ownership of the value.

There are two kinds of references in Rust:

  • shared references, indicated by &numbers
  • exclusive references, indicated by &mut numbers

Shared references allow many users to access the data at the same time. Exclusive references only allow a single user at a time.

We can rewrite our function to accept shared references as arguments, since we don’t need to mutate the Vec, and pass in shared references to the data in numbers.

fn main() {
let numbers = vec![10, 20, 30];
prints_numbers(&numbers);
prints_numbers(&numbers);
}

fn prints_numbers(vec_of_numbers: &Vec<i32>) {
dbg!(vec_of_numbers);
}

This lets us avoid cloning and creates clear expectations for the prints_numbers function. Since the prints_numbers function only accepts a shared reference, it can not mutate the Vec<i32>.