Static Ownership
Both tracing and reference counting detect garbage at runtime. Surely runtime detection is a requirement of any garbage collector. There has to be garbage before it can be detected, right? No. Rust detects at compile time when memory will become garbage. It does this by imposing a notion of single ownership on allocated memory.
When a normal variable is bound to a value, that variable becomes its sole owner. In this code, the message
variable is the owner of a heap string:
{
let message = String::from("You are not alone.");
} // scope closes, and message is automatically freed
A value has exactly one owner. The compiler examines the lifetime of this owner by inspecting (but not running) the source code. When that owner's lifetime ends due to scope changes or reassignment, the compiler inserts a free call in the generated machine code. A Rust program does not experience the unpredictable interruptions of tracing nor does it maintain counts of an object's references. The free calls are all inserted through the compiler's static analysis, not runtime checks.
This collection strategy is viable only because values have a single owner. If multiple owners were allowed, identifying an allocation as garbage would be incredibly complex. But how can a value have only a single owner? How can data be shared with functions if it is owned by the caller? How can objects be added and retrieved from a collection?
Moving
A value has only one owner, but ownership may change hands by moving the value to a different owner. Movement happens in two situations. The assignment operation in this code moves the heap string from its original owner verb
to its new owner label
:
let verb = String::from("submit");
let label = verb;
Additionally, passing a value as an actual parameter is by default a move to the formal parameter. In this code, label
becomes the new owner of the heap string upon the function call:
fn main() {
let verb = String::from("submit");
show_button(verb);
}
fn show_button(label: String) {
// ...
}
You've seen statements like label = verb
in other languages, in which label
becomes just another alias to the source object. For example, this C code makes q
an alias of p
:
int *p = (int *) malloc(sizeof(int) * 50);
int *q = p;
Aliasing is trouble. We might free an object through one alias and assign it NULL
, but the other alias sticks around as a dangling pointer.
Moving a value in Rust has different semantics than creating an alias. When a value is moved, the old owner becomes invalid. If we try to do anything with the old owner, the compiler rejects our code. This code, for example, does not compile:
let verb = String::from("submit");
let label = verb;
println!("{}", verb); // compile error, verb has no value
The error goes away if we print verb
prior to the label
assignment. At that point, verb
is still the owner.
Borrowing
A consequence of movement is that we lose ownership of a value when we pass it to a function. Imagine if telling someone your phone number meant that they then owned your phone number. You'd probably never share your number. Similarly, we might never want to call a function if it means losing ownership of our variables.
Rust provides a borrowing mechanism so that we can share a value without transferring ownership. To make a function borrow rather than own a parameter, we prefix both the actual parameter expression and the formal parameter type with the reference operator &
. In this program, main
lends a string to report
but retains ownership:
fn main() {
let weather = String::from("spiders");
report(&weather);
weather.clear(); // ok, weather is still an owner
weather.push_str("clear");
}
fn report(conditions: &String) {
println!("The weather is {}.", conditions);
}
Borrowed references are implemented via pointers, which are cheap to copy. But references in Rust do not have address semantics. We cannot use them to access memory directly, and we cannot apply arithmetic to move the pointer to neighboring memory cells.
The compiler ensures that no reference outlives its owner. In this code, reference y
borrows x
and is still active when x
gets reassigned:
let mut x = Vec::<u32>::new();
let y = &x;
x = Vec::<u32>::new(); // compile error, x loses its Vec,
println!("{:?}", y); // but y depends on x
The compiler rejects this code because borrowed y
outlives the original vector. However, if we flip the order of the last two statements, the code compiles and runs just fine. The lifetime of y
would end before x
is reassigned.
Mutable References
We cannot change a value through a plain reference, even if the owning variable is mutable. This code fails to compile because it calls the mutating method push_str
on immutable reference y
:
let mut x = String::from("ear");
let y = &x;
y.push_str("ring"); // compile error
If a reference is immutable, then the borrowing code can only read and not edit the value. This is a severe limitation. Good news. We create mutable references using the &mut
operator. This code changes the reference operator &
to the mutable reference operator &mut
:
let mut x = String::from("ear");
let y = &mut x;
y.push_str("ring"); // mutates the string
If we create a mutable reference, no other reference may be active at the same time, not even an immutable one. This code fails to compile because immutable y
is still active when mutable z
is declared:
let mut x = String::from("ear");
let y = &x;
let z = &mut x; // compile error
println!("{}", y);
If the println!
call was removed or even moved up before the declaration of z
, then y
would no longer be considered active when z
was declared, and the code would compile.
If we could have two mutable references active at the same time, then there's a chance that simultaneous edits would put the value in an inconsistent state. Some languages allow simultaneous edits and put the burden of synchronizing them on the programmer. Rust's borrow checker prevents simultaneous editors from even existing.
If we could have a mutable reference and an immutable reference active at the same time, then the entity with the immutable reference would be surprised to find the value that it believes immutable has been mutated.
Your past experience with permissive languages will likely get you in trouble with Rust's borrow checker. Stick to these principles to stay in its good graces:
- Give each variable the narrowest scope possible.
- Minimize how much the scopes of simultaneous references overlap.
- Use immutable values as much as possible.
Copy Semantics
Sometimes we might wish to make an independent copy of a value instead of moving it from one owner to another. In fact, this is what happens with primitive data and simple types like tuples made of primitives. This code creates two separate u32
instances and increments them:
let mut m = 17;
let mut n = m;
m += 1; // 18
n -= 1; // 16
Variable m
starts off with the value 17, and n
gets an independent copy of this value. Changes to one copy are not reflected in the other.
More complex types are moved rather than copied because copying large objects is expensive. But we can force a copy by calling the clone
method. This code creates and edits two separate strings, with variable n
getting its own copy of the string content of m
:
let mut m = String::from(":");
let mut n = m.clone();
m.push_str(")"); // m is :)
n.push_str("("); // n is :(
We can ask the compiler to automatically generate clone
methods for our custom types using traits, which we'll examine next chapter.