Ownership and Borrowing
Learning objectives
- You know the principles of Rust's ownership system.
- You know how to use references to borrow values.
- You know how to work with the compiler to satisfy the rules of ownership and references.
We managed to get through a lot of basic concepts in the previous part, all the while wholly avoiding the concept of ownership. Ownership is Rust's unique and essential feature that ensures memory safety without runtime costs — unlike the common alternative garbage collection. Understanding ownership can be a bit tricky at first, but if we don't understand Rust's ownership rules, we'll have a hard time satifying the Rust compiler's strict requirements.
There's no need to worry though! As experience with Rust accumulates, the ownership system will become more and more intuitive, and working with it can even boost understanding of how computers and memory work. Also, we don't need a deep understanding of how memory works under the hood to be able to create safe yet performant code in Rust — thanks to the helpful hints from the compiler, abstraction layers provided by Rust's standard library and the vast number of community created libraries. Indeed, here we'll cover the mere basics so that we can get on with programming (safely!) in Rust. Later on in the course we'll come back to ownership and memory management to look at it in more detail for some of the more advanced topics.
Before we go into ownership however, we'll first look at scope — a concept that plays a major role in Rust's ownership system — and how it affects variables in Rust.
Scope
Variables are only accessible in the scope they are defined in. This is called lexical (aka static) scoping. Each function has its own scope, and variables that are defined in the function are accessible only inside that function.
Scopes form a hierarchy where outer scopes cannot see variables from the inner subscopes. To demonstrate this, we can create extra scopes by writing block expressions with {}
.
Block expressions, like other expressions, have a type and evaluate to a value. To "return" a value from a block expression, we can leave the last expression within the block without a semicolon like with functions.
Note that we need to add the semicolon at the end of the let binding just like with the simpler let statements like let value = 3;
.
Blocks being expressions is a powerful feature in Rust which allows for writing neat syntax. The same rules for block expressions apply regardless of where the block is, be it a conditional expression, a loop or any other block.
On a side note but related to scopes, Rust supports shadowing. Shadowing allows reusing the same variable name for another variable. Shadowing a variable from the outer scope inside a subscope does not change the variable.
Question not found or loading of the question is still in progress.
Ownership
Rust follows a new and unique programming paradigm called ownership. It is the core feature that allows Rust to perform fast and enforce safety rules at compile-time.
We won't delve deeper into the intrinsics of how programming languages work, or why ownership does not slow down the program at runtime while keeping the program memory safety in check. For now, we will merely familiarize ourselves with the rules of ownership and how to work with the compiler to satisfy the rules.
The rules of ownership are threefold:
- Each value in Rust is owned by a variable.
- Each value can have only one owner at a time.
- When the owner goes out of scope, the value will be dropped.
Notice that the rule number three is about scopes. We already saw some of that rule in action when looking at how scopes affect variables. This rule helps ensure that when a value is no longer needed, the memory used to store it is automatically freed.
Let's see the first two rules in action with some code.
The ownership of String
To demonstrate the first two rules, we'll be using the struct String
. The string literals that we have been using so far are hard-coded and immutable. So we'll need a more complex type like String
whenever we want to modify a string or construct one during runtime (consider for example reading user input or files for processing).
There are many ways to create a String
struct. Two common ones are the String::from
method or the to_string
method on a string literal.
Let's then demonstrate the first two rules with some String
s. Run the following code to see that it has a problem.
The string owned by the variable x
is moved into the variable y
. Moving means changing ownership. After the value is moved, its old owner variable gets invalidated and cannot be used anymore.
Copying and cloning
Now let's look at another example to see what happens when we do the same with an integer.
In this example, new_variable
gets assigned the value of variable
, i.e. the value 5. Here, the old variable
is not invalidated, nor does changing the old variable
affect the value of the new_variable
. Remember that both integers must have a unique owner (rule 2 of ownership).
Now one may wonder, why does this work differently for integers and strings? The answer is that integers are copied when assigned to a new variable, while String
s are moved. Because the value is copied, the old owner variable is still valid as it still owns its value. This begs the question: which types are copied and which types are moved? The rough rule of thumb is that values that are cheap to copy (like integers and other scalar primitives) are copied, and values that are expensive to copy are moved.
Values such as String
which can be expensive to copy can be copied explicitly using the clone
method. This is referred to as cloning to make a distinction to the implicit copy.
Puzzle not found or loading of the puzzle is still in progress.
Explaining the rules
The first rule lets the compiler keep track of each value through their owner. The second rule is to ensure that a value within a variable won't be changed without explicitly moving or mutating the value. The second rule ensures also that values can be safely freed once the owner goes out of scope (the memory will be freed only once). The third rule guarantees that memory reserved for a variable is freed when the variable is no longer accessible. Since variables are dropped automatically after the owner goes out of scope, we don't need to remember to free the reserved memory.
The Rust compiler enforces memory safety according to the ownership rules at compile-time through its borrow checker, thus reducing the time spent debugging memory issues.
Question not found or loading of the question is still in progress.
What about functions?
Now let us look at what happens when we pass a String
to a function. According to what we have learned so far, a String
should be moved into the function when passing it to a function because we bind its value to the parameter of the function. Then, since the parameter variable has the ownership of the value, the old owner variable should be invalidated and can no longer be used.
Try running the code below to see that this indeed is what happens.
We should know by now that passing a String
to a function moves the value into the function scope and also that values gets dropped when they go out of scope. So how can we still use a value from a function scope after the function call? Well, simply by returning the value. The returned value can be moved out of the function and into the caller's scope the same way values are moved from one variable to another.
If we didn't capture the value of the returned String
in the y
variable, the return value would have been dropped when the function call ended. This is because the ownership wouldn't have changed as there was no move from one variable to another.
Some unknowns regarding ownership still remain though. What if we want to mutate copiable values like integers or floats in a function?
The code compiles just fine, but the value of x
is not changed. This is because the value of x
is copied into the function, and the function only mutates the copy of the value. To make the example function work according to our intent, we can return the mutated value from the function and assign it to the variable that we want to mutate.
Borrowing and dereferencing
Constantly moving ownership of values back and forth between functions can quickly become tedious. Instead, we can pass a reference to a variable to a function and then use or mutate the value behind the reference. We'll be needing to understand how to work with references to be able to use Rust to its full potential, so let's dive into it.
References are values that enable indirect access to other values. They point, i.e. refer, to some data of another value rather than containing the data themselves.
In Rust, we can choose whether we want to pass the value or a reference to the value to a function. This is done by explicitly taking a reference of the value using the ampersand &
symbol, and then passing that instead of the actual value. Due to Rust's ownership paradigm, we call this borrowing since taking a reference does not change the owner.
The scope of references
When we borrow a book in real life, we can expect the owner of the book to not be able to change anything in the book while we have it borrowed. This works the same in Rust. While we are borrowing a value from the owner, we can trust that the value isn't modified behind the scenes. The following example shows what happens if we try to modify a value that is being borrowed.
While the scope of variables is limited to the block or function they are declared in, the scope of a reference starts from where it is defined and ends when it is last used.
We can fix the above example by reordering the lines so that the scope of the reference borrowed_book
(&book
) ends before changing the value of book
.
Another thing to keep in mind while borrowing is that references are not allowed to "leak" referenced data out of the scope of the owner variable. Otherwise, we could have a reference to a value that has been dropped and that would be an invalid reference. Using such a reference would be unsound since the referred memory is no longer reserved for the program.
When we want to use a value from an expression or a function that depends on borrowed data, we can use the to_owned
method of a borrow to create an owned copy of the borrowed data. On a &str
, the to_owned()
method returns a new String
, just like String::from
.
You may have come across the terms pass-by-reference or pass-by-value. Many garbage collected languages (like Java, Python, Go or Dart) support only pass-by-value, meaning that the value of the variable is always copied when passed to a function. Although, they do support pass-by-reference in a certain sense, since often the values passed in such languages are actually references to a memory address that holds the underlying value. The passed values are only shallowly copied to the function since only the reference is copied and not the underlying value. Anything behind the reference can be accessed (and changed if mutable) by following the reference to the value(s) it refers to.
Test out for example the following code in the Dart language.
Such only pass-by-value restriction is not the case in Rust, which aims to provide as much control as possible for the programmer. We get to choose when we want to copy or clone a value and when we want to just refer to the value.
Not using references only makes sense when the function needs to own the data passed to it. Otherwise we would have to clone the data which can be expensive, or pass around ownerships constantly.
Like ownership has its set of rules, so does borrowing. The two rules of references summarize when references to data are allowed to exist.
- At any given time, a value can have either one mutable reference
&mut
or any number of immutable references&
. - References must always be valid.
The first rule, together with disallowing mutation of the owner while a borrow is alive, ensures that we don't accidentally modify a value that is being used or modified somewhere else (especially problematic in multi-threaded applications with concurrent mutating of values). The second rule ensures that we don't have references to values that have already been dropped.
Mutable borrows
Mutable variables can be borrowed mutably as mentioned in the first rule of references. This requires explicitly typing mut
after the reference operator just like when declaring a variable mutable. In order to change a mutably borrowed value, we must first dereference the value. Dereferencing is done by adding an asterisk *
before a reference.
The first rule of references dictates that we may not have other references to the same data while we have a mutable reference to it. The following example demonstrates how even the order in which values are read on a single line matters.
The scope of a reference ends when it is used for the last time and the arithmetic expression is read from left to right, so the code can be fixed by simply changing the order of *y
and x
in *y + x
. The scope of reference y
(the mutable reference to x
) ends in the expression *y
, which happens before reading x
in + x
, and thus the rules of references are satisfied.
Puzzle not found or loading of the puzzle is still in progress.
Borrowing or using data is fine after borrowing it mutably if the scope of the mutable borrow ends before such operations.
Note how in the line s.push_str("💕");
we don't need to dereference s
. This is because references are implicitly dereferenced when calling a method of the referred value's type.
Question not found or loading of the question is still in progress.
Phew! Now we have finally covered the essentials of Rust's ownership and references, which should be enough for most use cases with basic programming in Rust. As a reassurance if this all feels a bit much, rest assured the compiler will most of the time hint you to the right solution and you'll become better in time.
Summary of symbols
Symbol | Description |
---|---|
& | When in front of a value, borrows it, i.e. takes a reference to it. |
&mut | When in front of a value, borrows it mutably, i.e. takes a mutable reference to it. |
* | When in front of a reference, dereferences it to the value behind the reference. |
Hi! Please help us improve the course!
Please consider the following statements and questions regarding this part of the course. We use your answers for improving the course.
I can see how the assignments and materials fit in with what I am supposed to learn.
I find most of what I learned so far interesting.
I am certain that I can learn the taught skills and knowledge.
I find that I would benefit from having explicit deadlines.
I feel overwhelmed by the amount of work.
I try out the examples outlined in the materials.
I feel that the assignments are too difficult.
I feel that I've been systematic and organized in my studying.
How many hours (estimated with a precision of half an hour) did you spend reading the material and completing the assignments for this part? (use a dot as the decimal separator, e.g 8.5)
How would you improve the material or assignments?