Ownership and Borrowing
Learning Objectives
- You know the principles of ownership and borrowing.
- 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.
Ownership
Ownership is unique feature in Rust that ensures memory safety at compile-time without runtime costs — unlike the common alternative garbage collection.
We won’t go deeper into 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.
There are three rules of ownership, which are as follows:
- 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 — a variable defined inside a block was not available outside the block. 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.
Structs and ownership
To demonstrate the first two rules, we’ll be using the struct String
. The struct String
is needed 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 slice &str
.
Let’s then demonstrate the first two rules with some String
s. Run the following code to see that it has a problem.
The problem above relates to the rule 2 — each value can have only one owner at a time. As the value of x
is moved into y
, the old owner variable x
is invalidated and cannot be used anymore. Moving (or transferring) ownership is a fundamental concept in Rust. After a value is moved, its old owner variable gets invalidated and cannot be used anymore.
Primitive values and copying
Wait what?! Didn’t we just see variable reassignment in the previous chapter, where we assigned a new value to a variable? Yes, we did, but that was a special case.
Rust has a feature called implicit copy that allows certain types to be copied when assigned to a new variable. This is the case with primitive types like integers, floats, booleans, and characters.
More specifically, types that implement the
Copy
trait are copied when assigned to a new variable. TheCopy
trait is implemented for types that are cheap to copy, i.e. types that are stored on the stack and don’t require any cleanup when they go out of scope.
Due to the implicit copying, the following code works just fine.
Structs and cloning
If we would want to create a copy of a struct, we would have to use the clone
method. The clone
method creates a new instance of the struct with the same values as the original instance. This is called cloning to make a distinction to the implicit copy.
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.
In the following example, we clone a string during assignment to a new variable. This way, the value of the original variable is cloned and the original variable can still be used.
We will go over more details about copying and cloning in the memory management part of the course.
The three rules of ownership
-
Each value is owned by one and only one variable. This allows the compiler to track each value through its owner, ensuring that there’s no ambiguity about who holds responsibility for the data.
-
A value cannot be altered unless it is explicitly moved or mutated. This ensures that ownership is clear and controlled, preventing multiple entities from altering the same value. Moreover, it guarantees that values are safely freed once the owner goes out of scope.
-
A value will be dropped once it’s owner goes out of scope. This guarantees that memory reserved for a variable is freed when it is no longer needed, preventing memory leaks and allowing resource utilization optimization.
The Rust compiler enforces memory safety according to the ownership rules at compile-time, thus reducing the time spent debugging memory issues.
Functions and ownership
Based on the three rules of ownership, we can deduce how values are passed to functions in Rust. When a value is passed to a function, it is moved into the function scope. This means that the ownership of the value is transferred from the caller to the function. The function can then use the value, but the caller can no longer use it.
Try running the code below to see that this indeed is what happens.
As we learned before, we can solve the problem by creating a clone of the String
before passing it to the function. This way, the value of x
is cloned and the clone is moved into the function, leaving the original value of x
intact.
For primitive values and more broadly for values that implement the copy trait, we can pass the value to a function without worrying about moving the value. This is because the value is copied when passed to the function.
To use a value from a function after the function call, we can return the value from the function. By returning a value from a function, the returned value can be moved out of the function scope into the caller’s scope.
However, if we do not capture the value of the returned String
(as done into the variable y
above), the return value is dropped when the function call ends. This is because the ownership would not change, as there is 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.
If you have background in programming languages with low-level memory access like
C
orC++
, this may sound familiar. In those languages, we can pass a pointer to a variable to a function, and then mutate the value behind the pointer — a pointer is a memory address that points to a stored value. In Rust, we can do a similar thing with references, but we don’t have to worry about the memory safety issues that come with it, since the compiler will enforce the safety for us.
In any case, we’ll be needing to understand working with references in Rust to be able to use Rust to its full potential, so let’s dive into it.
Borrowing and dereferencing
Borrowing means creating a reference to a value instead of moving ownership. This way, the original owner keeps ownership, but another function or variable can temporarily “borrow” access.
There are two types of references in Rust: immutable references (&
) and mutable references (&mut
).
Immutable references
Immutable references let you read the value but not modify it. They are created with the &
symbol in front of a value.
Mutable references
Mutable references let you both read and modify the value. They are created with the &mut
symbol in front of a value, which must also be declared mut
.
Rules of borrowing
There are three rules of borrowing:
- At any given time, a value can have either one mutable reference (
&mut
) (a “writer”) or any number of immutable references (&
) (readers), but not both. - The original value cannot be used in a way that conflicts with any active borrows.
- References must always be valid.
The first rule ensures that data is either being read or written, but not both simultaneously. The second rule ensures that a borrowed value that’s being mutated is not used elsewhere at the same time. The third rule ensures that references do not point to values that have been dropped.
Ownership and borrowing rules are enforced at compile time, so Rust doesn’t need the runtime checks found in many other high-level safe languages.
Validity of references
According to the third rule, references must always be valid: a reference must always point to a valid value. If the value is dropped before the reference is used, the reference would be invalid. The following example shows what happens if you try to modify a value while it’s currently borrowed:
Reference scopes
The scope of a reference begins where it is defined and ends when it is last used, which differs from how variables are scoped to a block. This means we can fix the previous example by reordering lines so that the reference borrowed_book
ends before we modify book
:
References also cannot outlive their owner’s scope. Otherwise, a reference could point to dropped data. The following example demonstrates this:
If you need to use data from an expression or function after the original data is dropped, you can use the to_owned method to create an owned copy. For a &str
, to_owned()
returns a new String
, similar to String::from
.
Dereferencing mutable references
In our first mutable reference example, we appended a string to the borrowed value:
This works because Rust implicitly dereferences the reference when calling methods on the referenced type. However, if we wanted to replace the entire book via borrowed_book = "📘";
, that wouldn’t work directly.
The borrowed variable is distinct from the original variable.
To change a mutable borrowed value directly, you must dereference it with *
. For example:
Here’s the same idea with a primitive value:
The next example explores the second rule of borrowing (you cannot use the original value in a way that conflicts with an active mutable borrow). Rust enforces exclusive access to a value while it’s mutably borrowed, preventing other references:
The scope of y
ends when it is last used, which in the code above is effectively at the same point we try to use x
. We can fix this by changing the order of operations so the mutable borrow ends sooner:
As an extension to mutable dereferencing, Rust lets you use the dereference operator *
even for non-mutable references when you only need to read the underlying value:
Rust also provides the Deref
trait, which allows “smart” pointer types to behave like regular references.
Functions that borrow data
Consider the following example where ownership is transferred between functions:
Now that we know about references, we can improve the code by borrowing the data instead of moving it. This allows us to keep the original data after the function call. We just need to adjust the function signatures to reflect borrowing:
String slices (&str
)
As another example of borrowing, let’s look at string slices. A string slice &str
is a reference to part of a string. String literals (e.g., "hello world"
) have the type &str
, not String
.
The
str
type is a dynamically sized type, unlikeString
. This means you can’t have a variable of typestr
; you can only have&str
. Read more aboutstr
in the Rust documentation.
Passing by value only makes sense when a function needs to own the data. Otherwise, you’d have to clone the data (potentially expensive) or keep transferring ownership. By using &str
, you can pass around references efficiently:
Borrowing patterns
We’ve now covered the essentials of Rust’s ownership and references, which should suffice for most basic Rust programming. If it feels like a lot, don’t worry: the compiler will guide you in most cases, and your familiarity will grow with practice.
Here’s an example that shows more borrowing:
Notice that in s.push_str("💕");
, we didn’t need to write (*s).push_str(...)
because Rust implicitly dereferences references when calling methods.
Pass-by-value vs pass-by-reference
Many garbage-collected languages (like Java, Python, Go, or Dart) support only pass-by-value in their function calls, meaning a variable’s value is always copied. However, these languages often use references behind the scenes, so only the reference is copied, not the underlying data. This allows the function to access (and mutate, if allowed) the same data via that copied reference.
For example, consider the following Dart code:
This “only pass-by-value” restriction does not apply to Rust, which aims to give programmers more control. We can choose when to copy or clone data and when to pass references instead.
Summary of symbols
Symbol | Description |
---|---|
& | Borrows a value immutably, i.e., takes an immutable reference to it. |
&mut | Borrows a value mutably, i.e., takes a mutable reference to it. |
* | Dereferences a reference, giving access to the underlying value. |