Borrowing
In the previous section, we learned that each resource has a unique owner. Ownership can be moved—for example, into a function.
In many situations, however, we do not want to permanently move a resource into a function. Instead, we want to retain ownership but allow the function to temporarily access the resource while it executes.
We could accomplish this by having each function agree to return resources of this
sort. For
example, take_and_return_ownership
below takes ownership of a string
resource and returns ownership of that exact same resource. The caller, main
,
assigns the returned resource to the same variable, s
.
This code prints hello
twice.
The type of
take_and_return_ownership
does not guarantee that the returned resource is the
same as the provided resource. Instead, the programmer has to trust that it returns
the same resource.
As code becomes more complex, this pattern of returning all of the provided resources explicitly becomes both syntactically and semantically unwieldy.
Fortunately, Rust offers a powerful solution: passing in arguments via a reference. Taking a reference does not change the owner of a resource. Instead, the reference simply borrows access to the resource temporarily. Rust's borrow checker requires that references to resources do not outlive their owner, to avoid the possibility of there being references to resources that the ownership system has decided can be dropped.
There are two kinds of borrows in Rust, immutable borrows and mutable borrows. These differ in how much access to the resource they provide.
Immutable Borrows
In the following example, we define a function, f
, that takes an immutable
reference to a String
, which has type &String
, as input. It then de-references
the immutable reference, written *s
, in order to print it.
When the main
function calls f
, it must provide a reference to a String
as
an argument. Here, we do so by taking a reference to the let-bound variable x
on Line 3, written &x
. Taking a reference does not cause a change in
ownership, so x
still owns the string resource in the remainder of main
and it can, for example, print x
on Line 4. The resource will be dropped when
x
goes out of scope at the end of main
as we discussed previously. Because f
takes a reference, it is only borrowing access to the resource that the
reference points to. It does not need to explicitly return the resource because
it does not own it. Rust knows that the borrow does not outlive the owner
because the borrow is no longer accessible after f
returns.
This code prints hello
twice.
Note: you do not actually need to dereference s
to pass it to println!
in Rust:
it is a macro, so it will automatically dereference or borrow as needed
to ensure that a move is not needed. Indeed, Rust does a lot of implicit
borrowing and dereferencing to make its syntax simple, as we will see in other examples
below.
Methods of the String
type, like len
for computing the length, typically
take their arguments by reference. You can call a method explicitly with a
reference, e.g. String::len(&s)
. As shorthand, you can use dot notation to
call a method, e.g. s.len()
. This implicitly takes a reference to s
.
This code prints len1 = 5 = len2 = 5
.
You can keep multiple immutable borrows live at the same time, e.g. y
and z
in the following example are both live as shown in the visualization. For this
reason, immutable borrows are also sometimes called shared borrows: each
immutable reference shares access to the resource with the owner and with any
other immutable references that might be live.
This code prints hello and hello
.
Ownership of a resource cannot be moved while it is borrowed. For example, the following is erroneous:
fn main() { let s = String::from("hello"); let x = &s; let s2 = s; // ERROR: cannot move s while a borrow is live println!("{}", String::len(x)); }
The compiler error here is: cannot move out of s because it is borrowed
.
Mutable Borrows
Unlike immutable borrows, Rust's mutable borrows allow you to mutate the
borrowed resource. In the example below, we push (copy) the contents of a String
s2
to the end of the heap-allocated String
s1
twice, first by explictly calling
the String::push_str
method, and then using the equivalent shorthand method
call syntax. In both cases, the method takes a mutable reference to s1
,
written explicitly &mut s1
.
This code prints Hello, world, world
.
Code that does a lot of mutation is notoriously difficult to reason about, so in Rust, mutation is much more carefully controlled than in other imperative languages.
First, you can only take a mutable borrow from a mutable variable, i.e. one
bound using let mut
like s1
in the example above. Immutability is the
default in Rust because it is considered easier to reason about.
Second, mutable borrows are unique—you cannot take a borrow, mutable or immutable, if any mutable borrow is live. This means that you can be certain that no other code will be mutating a resource when you have mutably borrowed it. For this reason, mutable borrows are also sometimes called unique borrows.
For example, the following code is erroneous because a mutable borrow, y
, is
live.
fn main() { let mut x = String::from("hello"); let y = &mut x; f(&x); // ERROR: y is still live String::push_str(y, ", world"); } fn f(x : &String) { println!("{}", x); }
The compiler error here is: cannot borrow x as immutable because it is also borrowed as mutable
.
The following code is erroneous for the same reason.
fn main() { let mut x = String::from("Hello"); let y = &mut x; let z = &mut x; // ERROR: y is still live String::push_str(y, ", world"); String::push_str(z, ", friend"); println!("{}", x); }
The compiler error here is: cannot borrow x as mutable more than once at a time
.
Optional: Threading in Rust
In the example above, the two calls to push_str
are sequenced. However, if we
wanted to execute them concurrently, we could do so by spawning a thread as
follows. Here, || { e }
is Rust's notation for an anonymous function taking
unit input.
use std::thread; fn main() { let mut x = String::from("Hello"); let y = &mut x; let z = &mut x; // NOT OK: y is still live thread::spawn(|| { String::push_str(y, ", world"); }); String::push_str(z, ", friend"); println!("{}", x); }
If the borrow checker did not stop us, this program would have a race
condition—it could print either Hello, world, friend
or Hello, friend, world
depending on the interleaving of the main thread and the newly spawned thread.
By tightly controlling mutation, Rust prevents races mediated by shared mutable state.
(The topic of parallelism and concurrency in Rust will be explored further in A9!)
Non-Lexical Lifetimes
Above, we use the phrase "live borrow". A borrow is live if it is in scope and there remain future uses of the borrow. A borrow dies as soon it is no longer needed. So the following code works, even though there are two mutable borrows in the same scope:
This code prints Hello, world, world!!
.