TL;DR
Three access patterns in Rust:
- Ownership
- Shared read-only access
- Exclusive mutable access
A lifetime is some stretch of your program for which a reference could be safe to use: a statement, an expression, the scope of some variable, or the like. Lifetimes are entirely figments of Rust’s compile-time imagination. At run time, a reference is nothing but an address; its lifetime is part of its type and has no run-time representation.
生命周期以 block 为单位。
You only need to worry about lifetime parameters when defining functions and types; when using them, Rust infers the lifetimes for you.
As long as there are shared references to a value, not even its owner can modify it; the value is locked down. Throughout its lifetime, a shared reference makes its referent read-only: you may not assign to the referent or move its value elsewhere.
If there is a mutable reference to a value, it has exclusive access to the value; you can’t use the owner at all, until the mutable reference goes away.
Besides references that are simple addresses, Rust also includes two kinds of fat pointers, two-word values carrying the address of some value, along with some further information necessary to put the value to use.
- A reference to a slice is a fat pointer: a two-word value comprising a pointer to the slice’s first element, and the number of elements in the slice.
- Rust’s other kind of fat pointer is a trait object, a reference to a value that implements a certain trait.
- A trait object carries a value’s address and a pointer to the trait’s implementation appropriate to that value, for invoking the trait’s methods.
Rust’s references are always tied to a particular lifetime, making it feasible to check them at compile time (check lifetime overlap).
In Rust, if you need a value that is either a reference to something or not, use the type Option<&T>
. At the machine level, Rust represents None
as a null pointer, and Some(r)
, where r
is a &T
value, as the nonzero address, so Option<&T>
is just as efficient as a nullable pointer in C or C++, even though it’s safer: its type requires you to check whether it’s None
before you can use it.
Brief
All the pointer types we’ve seen so far—the simple Box<T>
heap pointer, and the pointers internal to String
and Vec
values are owning pointers: when the owner is dropped, the referent goes with it.
Rust also has non-owning pointer types called references, which have no effect on their referents’ lifetimes. References must never outlive their referents. You must make it apparent in your code that no reference can possibly outlive the value it points to.
To emphasize this, Rust refers to creating a reference to some value as borrowing the value: what you have borrowed, you must eventually return to its owner.
The references themselves are nothing special—under the hood, they’re just addresses. But the rules that keep them safe are novel to Rust. And although these rules are the part of Rust that requires the most effort to master, the breadth of classic, absolutely everyday bugs they prevent is surprising, and their effect on multithreaded programming is liberating. This is Rust’s radical wager, again.
- These rules prevent some common categories of bugs at compile time and without run-time performance penalties.
References to Values
There’s a hash table that maps String
values to Vec<String>
values, taking the name of an artist to a list of the names of their works.
|
|
The definition for show
raises a few questions. HashMap
is not Copy
, since it owns a dynamically allocated table. So when the program calls show(table)
, the whole structure gets moved to the function, leaving the variable table
uninitialized. The outer for
loop takes ownership of the hash table and consumes it entirely; and the inner for
loop does the same to each of the vectors. Because of move semantics, we’ve completely destroyed the entire structure simply by trying to print it out.
The right way to handle this is to use references. A reference lets you access a value without affecting its ownership. References come in two kinds:
- A shared reference lets you read but not modify its referent.
- You can have as many shared references to a particular value at a time.
- The expression
&e
yields a shared reference toe
’s value; ife
has the typeT
, then&e
has the type&T
, pronounced “refT
.” - Shared references are
Copy
.
- If you have a mutable reference to a value, you may both read and modify the value.
- You may not have any other references of any sort to that value active at the same time.
- The expression
&mut e
yields a mutable reference toe
’s value; you write its type as&mut T
, which is pronounced “ref muteT
.” - Mutable references are not
Copy
.
Think of the distinction between shared and mutable references as a way to enforce a multiple readers or single writer rule at compile time. In fact, this rule doesn’t apply only to references; it covers the borrowed value’s owner as well. As long as there are shared references to a value, not even its owner can modify it; the value is locked down. Nobody can modify table
while show
is working with it. Similarly, if there is a mutable reference to a value, it has exclusive access to the value; you can’t use the owner at all, until the mutable reference goes away.
Keeping sharing and mutation fully separate turns out to be essential to memory safety.
The printing function in our example doesn’t need to modify the table, just read its contents. So the caller should be able to pass it a shared reference to the table.
|
|
References are non-owning pointers, so the table
variable remains the owner of the entire structure; show
has just borrowed it for a bit. The type of show
’s parameter table
has changed from Table
to &Table
: instead of passing the table
by value (and hence moving ownership into the function), we’re now passing a shared reference.
Iterating over a shared reference to a HashMap
is defined to produce shared references to each entry’s key and value: artist
has changed from a String
to a &String
, and works
from a Vec<String>
to a &Vec<String>
. The inner loop is changed similarly. Iterating over a shared reference to a vector is defined to produce shared references to its elements, so work
is now a &String
.
When we pass a value to a function in a way that moves ownership of the value to the function, we say that we have passed it (the value) by value. If we instead pass the function a reference to the value, we say that we have passed the value by reference.
Working with References
Rust References Versus C++ References
Rust references and C++ references are both just addresses at the machine level.
In C++, references are created implicitly by conversion, and dereferenced implicitly too:
|
|
In Rust, references are created explicitly with the &
operator, and dereferenced explicitly with the *
operator:
|
|
However, the .
operator is an exception. Since references are so widely used in Rust, the .
operator implicitly dereferences its left operand, if needed:
|
|
- The
println!
macro used in theshow
function expands to code that uses the.
operator, so it takes advantage of this implicit dereference as well.
The .
operator can also implicitly borrow a reference to its left operand, if needed for a method call:
|
|
In a nutshell, whereas C++ converts implicitly between references and lvalues (that is, expressions referring to locations in memory), with these conversions appearing anywhere they’re needed, in Rust you use the &
and *
operators to create and follow references, with the exception of the .
operator, which borrows and dereferences implicitly.
Assigning References
In Rust, assigning a reference to a variable makes that variable point somewhere new:
|
|
The reference r
initially points to x
. But if b
is true, the code points it at y
instead.

C++ references behave very differently: assigning a value to a reference in C++ stores the value in its referent. Once a C++ reference has been initialized, there’s no way to make it point at anything else.
|
|
References to References
Rust permits references to references:
|
|
The reference types are written out for clarity. The .
operator follows as many references as it takes to find its target.

The expression rrr.y
, guided by the type of rrr
, actually traverses three references to get to the Point
before fetching its y
field.
Comparing References
Like the .
operator, Rust’s comparison operators “see through” any number of references:
|
|
The final assertion here succeeds, even though rrx
and rry
point at different values (namely, rx
and ry
), because the ==
operator follows all the references and performs the comparison on their final targets, x
and y
. This is almost always the behavior you want, especially when writing generic functions.
If you actually want to know whether two references point to the same memory, you can use std::ptr::eq
, which compares them as addresses:
|
|
The operands of a comparison must have exactly the same type, including the references.
|
|
References Are Never Null
Rust references are never null. There’s no analogue to C’s NULL
or C++’s nullptr
. There is no default initial value for a reference (you can’t use any variable until it’s been initialized, regardless of its type) and Rust won’t convert integers to references (outside of unsafe
code), so you can’t convert zero into a reference.
C and C++ code often uses a null pointer to indicate the absence of a value: for example, the malloc
function returns either a pointer to a new block of memory or nullptr
if there isn’t enough memory available to satisfy the request.
In Rust, if you need a value that is either a reference to something or not, use the type Option<&T>
. At the machine level, Rust represents None
as a null pointer, and Some(r)
, where r
is a &T
value, as the nonzero address, so Option<&T>
is just as efficient as a nullable pointer in C or C++, even though it’s safer: its type requires you to check whether it’s None
before you can use it.
Borrowing References to Arbitrary Expressions
Whereas C and C++ only let you apply the &
operator to certain kinds of expressions, Rust lets you borrow a reference to the value of any sort of expression at all.
|
|
In situations like this, Rust simply creates an anonymous variable to hold the expression’s value and makes the reference point to that. The lifetime of this anonymous variable depends on what you do with the reference:
- If you immediately assign the reference to a variable in a
let
statement (or make it part of some struct or array that is being immediately assigned), then Rust makes the anonymous variable live as long as the variable thelet
initializes. In our example, Rust would do this for the referent ofr
. - Otherwise, the anonymous variable lives to the end of the enclosing statement. In our example, the anonymous variable created to hold
1009
lasts only to the end of theassert_eq!
statement.
If you’re used to C or C++, this may sound error-prone. But remember that Rust will never let you write code that would produce a dangling reference. If the reference could ever be used beyond the anonymous variable’s lifetime, Rust will always report the problem to you at compile time. You can then fix your code to keep the referent in a named variable with an appropriate lifetime.
References to Slices and Trait Objects
Besides references that are simple addresses, Rust also includes two kinds of fat pointers, two-word values carrying the address of some value, along with some further information necessary to put the value to use.
- A reference to a slice is a fat pointer: a two-word value comprising a pointer to the slice’s first element, and the number of elements in the slice.
- Rust’s other kind of fat pointer is a trait object, a reference to a value that implements a certain trait.
- A trait object carries a value’s address and a pointer to the trait’s implementation appropriate to that value, for invoking the trait’s methods.
Aside from carrying this extra data, slice and trait object references behave just like the other sorts of references: they don’t own their referents, they are not allowed to outlive their referents, they may be mutable or shared, and so on.
Reference Safety
Borrowing a Local Variable
You can’t borrow a reference to a local variable and take it out of the variable’s scope:
|
|
Rust’s complaint is that x
lives only until the end of the inner block, whereas the reference remains alive until the end of the outer block, making it a dangling pointer.
Rust tries to assign each reference type a lifetime that meets the constraints imposed by how it is used. A lifetime is some stretch of your program for which a reference could be safe to use: a statement, an expression, the scope of some variable, or the like. Lifetimes are entirely figments of Rust’s compile-time imagination. At run time, a reference is nothing but an address; its lifetime is part of its type and has no run-time representation.
In this example, there are three lifetimes. The variables r
and x
both have a lifetime, extending from the point at which they’re initialized until the point that the compiler can prove they are no longer in use. The third lifetime is that of a reference type: the type of the reference we borrow to x
and store in r
.
Here’s one constraint that should seem pretty obvious: if you have a variable x
, then a reference to x
(&x
) must not outlive x
itself.

Beyond the point where x
goes out of scope, the reference &x
would be a dangling pointer. We say that the variable’s lifetime must contain or enclose that of the reference borrowed from it. This constraint limits how large a reference’s lifetime can be.
- reference 生命周期上限显然是它指向的 referent 的生命周期。
Here’s another kind of constraint: if you store a reference in a variable r
(makes r
&i32
type), the reference’s type (&i32
) must be good for the entire lifetime of the variable, from its initialization until its last use. If the reference (&x
) can’t live at least as long as the variable does, then at some point r
will be a dangling pointer. We say that the reference’s lifetime must contain or enclose the variable’s. This constraint limits how small a reference’s lifetime can be.
- 如果将 reference 保存在变量中,那 reference 的生命周期至少要和该变量一样长。

Rust simply tries to find a lifetime for each reference that satisfies all these constraints. In our example, however, there is no such lifetime.

The following example works out. The reference’s lifetime must be contained by x
’s, but fully enclose r
’s.

These rules apply in a natural way when you borrow a reference to some part of some larger data structure, like an element of a vector:
|
|
Since v
owns the vector, which owns its elements, the lifetime of v
must enclose that of the reference type of &v[1]
. Similarly, if you store a reference in some data structure, its lifetime must enclose that of the data structure. For example, if you build a vector of references, all of them must have lifetimes enclosing that of the variable that owns the vector.
The principle that Rust uses for all code: first, understand the constraints arising from the way the program uses references; then, find lifetimes that satisfy them. This is not so different from the process C and C++ programmers impose on themselves; the difference is that Rust knows the rules and enforces them.
Receiving References as Function Arguments
Suppose we have a function f
that takes a reference and stores it in a global variable:
|
|
Rust’s equivalent of a global variable is called a static: it’s a value that’s created when the program starts and lasts until it terminates. (Like any other declaration, Rust’s module system controls where statics are visible, so they’re only “global” in their lifetime, not their visibility.) There’re some rules for statics:
- Every static must be initialized.
- Mutable statics are inherently not thread-safe. Any thread can access a static at any time. Even in single-threaded programs, they can fall prey to other sorts of reentrancy problems. For these reasons, you may access a mutable
static
only within anunsafe
block.
Revised version with unsafe
(still not good enough)):
|
|
To see the remaining problem, we need to write out a few things that Rust is helpfully letting us omit. The signature of f
as written here is actually shorthand for the following:
|
|
Here, the lifetime 'a
(pronounced “tick A”) is a lifetime parameter of f
. You can read <'a>
as “for any lifetime 'a
”. So when we write fn f<'a>(p: &'a i32)
, we’re defining a function that takes a reference to an i32
with any given lifetime 'a
.
Since we must allow 'a
to be any lifetime, things had better work out if it’s the smallest possible lifetime: one just enclosing the call to f
. The assignment STASH = p;
raises some problem. Since STASH
lives for the program’s entire execution, the reference type it holds must have a lifetime of the same length; Rust calls this the 'static
lifetime. But the lifetime of p
’s reference is some 'a
, which could be anything, as long as it encloses the call to f
. So, Rust rejects our code. At this point, it’s clear that our function can’t accept just any reference as an argument.
The following version compiles just fine:
|
|
f
’s signature spells out that p must be a reference with lifetime 'static
. We can only apply f
to references to other statics, but that’s the only thing that’s certain not to leave STASH
dangling anyway.
The original f(p: &i32)
ended up as f(p: &'static i32)
. In other words, we were unable to write a function that stashed a reference in a global variable without reflecting that intention in the function’s signature. In Rust, a function’s signature always exposes the body’s behavior.
Conversely, if we see a function with a signature like g(p: &i32)
(or with the lifetimes written out, g<'a>(p: &'a i32)
), we can tell that it does not stash its argument p
anywhere that will outlive the call. There’s no need to look into g
’s definition; the signature alone tells us what g
can and can’t do with its argument.
Passing References to Functions
Consider the following code:
|
|
From g
’s signature alone, Rust knows it will not save p
anywhere that might outlive the call: any lifetime that encloses the call must work for 'a
. So Rust chooses the smallest possible lifetime for &x
: that of the call to g
. This meets all constraints: it doesn’t outlive x
, and it encloses the entire call to g
. So this code passes muster.
Although g
takes a lifetime parameter 'a
, we didn’t need to mention it when calling g
. You only need to worry about lifetime parameters when defining functions and types; when using them, Rust infers the lifetimes for you.
If we tried to pass &x
to our function f
that stores its argument in a static, it fails to compile: the reference &x
must not outlive x
, but by passing it to f
, we constrain it to live at least as long as 'static
. There’s no way to satisfy everyone here, so Rust rejects the code:
|
|
Returning References
Here’s a function that returns a reference to the smallest element of a slice:
|
|
We’ve omitted lifetimes from that function’s signature in the usual way. When a function takes a single reference as an argument and returns a single reference, Rust assumes that the two must have the same lifetime.
From smallest’s signature, we can see that its argument and return value must have the same lifetime, 'a
. The argument ¶bola
must not outlive parabola
itself, yet smallest
’s return value must live at least as long as s
. There’s no possible lifetime 'a
that can satisfy both constraints.
Moving s
so that its lifetime is clearly contained within parabola
’s fixes the problem.
|
|
Lifetimes in function signatures let Rust assess the relationships between the references you pass to the function and those the function returns, and they ensure they’re being used safely.
Structs Containing References
Consider the following code we’ve put the reference inside a structure:
|
|
The safety constraints Rust places on references apply to S
as well. They don’t just disappear just because the reference is inside a struct. Somehow, those constraints must end up applying to S
as well.
Whenever a reference type appears inside another type’s definition, you must write out its lifetime. You can write this:
|
|
This says that r
can only refer to i32
values that will last for the lifetime of the program, which is rather limiting. The alternative is to give the type a lifetime parameter 'a
and use that for r
:
|
|
Each value you create of type S
gets a fresh lifetime 'a
, which becomes constrained by how you use the value. The lifetime of any reference &x
you store in r
had better enclose 'a
, and 'a
must outlast the lifetime of wherever you store the S
. The expression S { r: &x }
creates a fresh S
value with some lifetime 'a
. When you store &x
in the r
field, you constrain 'a
to lie entirely within x
’s lifetime.
S
的生命周期是'a
,和S.r
的一样;S { r: &x }
相当于S.r = &x
,&x
的生命周期必须大于S.r
的生命周期'a
;s = S { r: &x }
,S
的生命周期'a
必须大于s
;x
的生命周期必须大于&x
的生命周期;'a
>=s
,x
>=&x
>='a
,而s
的生命周期显然长于x
;
The assignment s = S { ... }
stores this S
in a variable whose lifetime extends to the end of the example code, constraining 'a
to outlast the lifetime of s
. And now Rust has arrived at the same contradictory constraints as before: 'a
must not outlive x
, yet must live at least as long as s
.
When a type (S
) with a lifetime parameter is placed inside some other type (D
), you also must specify its lifetime:
|
|
We can’t leave off S
’s lifetime parameter here: Rust needs to know how D
’s lifetime relates to that of the reference in its S
in order to apply the same checks to D
that it does for S
and plain references.
We could give s the 'static
lifetime:
|
|
The error message from Rust actually suggests another approach, which is more general:
With this definition, the s
field may only borrow values that live for the entire execution of the program. That’s somewhat restrictive, but it does mean that D
can’t possibly borrow a local variable; there are no special constraints on D
’s lifetime.
|
|
Here, we could give D
its own lifetime parameter and pass that to S
. By taking a lifetime parameter 'a
and using it in s
’s type, we’ve allowed Rust to relate D
value’s lifetime to that of the reference its S
holds.
A type’s lifetime parameters always reveal whether it contains references with interesting (that is, non-'static
) lifetimes and what those lifetimes can be.
Suppose we have a parsing function that takes a slice of bytes and returns a structure holding the results of the parse:
|
|
Without looking into the definition of the Record
type at all, we can tell that, if we receive a Record
returned from parse_record
, whatever references it contains must point into the input buffer we passed in, and nowhere else (except perhaps at 'static
values).
This exposure of internal behavior is the reason Rust requires types that contain references to take explicit lifetime parameters. There’s no reason Rust couldn’t simply make up a distinct lifetime for each reference in the struct and save you the trouble of writing them out. Early versions of Rust actually behaved this way, but developers found it confusing: it is helpful to know when one value borrows something from another value, especially when working through errors.
It’s not just references and types like S
that have lifetimes. Every type in Rust has a lifetime, including i32
and String
. Most are simply 'static
, meaning that values of those types can live for as long as you like. For example, a Vec<i32>
is self-contained and needn’t be dropped before any particular variable goes out of scope. A type like Vec<&'a i32>
has a lifetime that must be enclosed by 'a
: it must be dropped while its referents are still alive.
Distinct Lifetime Parameters
Consider the following code:
|
|
Both references of S
use the same lifetime 'a
. This code doesn’t create any dangling pointers. The reference to y
stays in s
, which goes out of scope before y
does. The reference to x
ends up in r
, which doesn’t outlive x
.
Follow this reasoning:
- Both fields of
S
are references with the same lifetime'a
, so Rust must find a single lifetime that works for boths.x
ands.y
. - Assigning
r = s.x
requires'a
to encloser
’s lifetime. Initializings.y
with&y
requires'a
to be no longer thany
’s lifetime.
No lifetime is shorter than y
’s scope but longer than r
’s. The problem arises because both references in S
have the same lifetime 'a
. Changing the definition of S
to let each reference have a distinct lifetime fixes everything.
|
|
With this definition, s.x
and s.y
have independent lifetimes. What we do with s.x
has no effect on what we store in s.y
: 'a
can simply be r
’s lifetime, and 'b
can be s
’s. (y
’s lifetime would work too for 'b
, but Rust tries to choose the smallest lifetime that works.)
Function signatures can have similar effects:
|
|
The downside to this is that adding lifetimes can make types and function signatures harder to read. Authors tend to try the simplest possible definition first and then loosen restrictions until the code compiles. Since Rust won’t permit the code to run unless it’s safe, simply waiting to be told when there’s a problem is a perfectly acceptable tactic.
Omitting Lifetime Parameters
Rust lets us omit lifetime for function parameters and return values when it’s reasonably obvious what they should be.
In the simplest cases, Rust just assigns a distinct lifetime to each spot that needs one:
|
|
If you do return references or other types with lifetime parameters, Rust still tries to make the unambiguous cases easy.
If there’s only a single lifetime that appears among your function’s parameters, then Rust assumes any lifetimes in your return value must be that one:
|
|
If there are multiple lifetimes among your parameters, then there’s no natural reason to prefer one over the other for the return value, and Rust makes you spell out what’s going on.
If a function is a method on some type and takes its self
parameter by reference, then Rust assumes that self
’s lifetime is the one to give everything in the return value.
|
|
Rust assumes that whatever you’re borrowing, you’re borrowing from self
.
These are just abbreviations, meant to be helpful without introducing surprises. When they’re not what you want, you can always write the lifetimes out explicitly.
Sharing Versus Mutation
Here’s one way to introduce dangling pointers:
|
|
The assignment to aside
moves the vector, leaving v
uninitialized, and turns r
into a dangling pointer.

Although v
stays in scope for r
’s entire lifetime, the problem here is that v
’s value gets moved elsewhere, leaving v
uninitialized while r
still refers to it.
Throughout its lifetime, a shared reference makes its referent read-only: you may not assign to the referent or move its value elsewhere. In this code, within r
’s lifetime there exists the attempt to move the vector, so Rust rejects the program.
The following version works.
|
|
r
goes out of scope earlier, the reference’s lifetime ends before v
is moved aside.
Another way to introduce dangling pointers:
|
|
When we add an element to a vector, if its buffer is full, it must allocate a new buffer with more space. Suppose wave
starts with space for four elements and so must allocate a larger buffer when extend
tries to add a fifth (extend(&mut wave, &wave);
). The extend
function’s vec
argument borrows wave
(owned by the caller), which has allocated itself a new buffer with space for eight elements. But slice
continues to point to the old four-element buffer, which has been dropped.
- 随着
extend
函数中vec.push
的执行,wave
中保存的堆地址更新了(wave
本身的地址不变),而slice
仍然指向旧的小空间的第一个元素的地址。

This sort of problem isn’t unique to Rust: modifying collections while pointing into them is delicate territory in many languages. In C++, the std::vector
specification cautions you that “reallocation [of the vector’s buffer] invalidates all the references, pointers, and iterators referring to the elements in the sequence.” Java says, of modifying a java.util.Hashtable
object: If the Hashtable
is structurally modified at any time after the iterator is created, in any way except through the iterator’s own remove
method, the iterator will throw a ConcurrentModificationException
.
What’s especially difficult about this sort of bug is that it doesn’t happen all the time. In testing, your vector might always happen to have enough space, the buffer might never be reallocated, and the problem might never come to light. Rust reports the problem with our call to extend
at compile time.
We may borrow a mutable reference to the vector, and we may borrow a shared reference to its elements, but those two references’ lifetimes must not overlap. In our case, both references’ lifetimes contain the call to extend
, so Rust rejects the code.
These errors both stem from violations of Rust’s rules for mutation and sharing:
Shared access is read-only access.
- Values borrowed by shared references are read-only. Across the lifetime of a shared reference, neither its referent, nor anything reachable from that referent, can be changed by anything. There exist no live mutable references to anything in that structure (ownership tree), its owner is held read-only, and so on. It’s really frozen.
Mutable access is exclusive access.
- A value borrowed by a mutable reference is reachable exclusively via that reference. Across the lifetime of a mutable reference, there is no other usable path to its referent or to any value reachable from there. The only references whose lifetimes may overlap with a mutable reference are those you borrow from the mutable reference itself.
Rust reported the extend
example as a violation of the second rule: since we’ve borrowed a mutable reference to wave
, that mutable reference must be the only way to reach the vector or its elements. The shared reference to the slice
is itself another way to reach the elements, violating the second rule.
Rust could also have treated our bug as a violation of the first rule: since we’ve borrowed a shared reference to wave
’s elements, the elements and the Vec
itself are all read-only. You can’t borrow a mutable reference to a read-only value.
Each kind of reference affects what we can do with the values along the owning path to the referent, and the values reachable from the referent.

In both cases, the path of ownership leading to the referent cannot be changed for the reference’s lifetime. For a shared borrow, the path is read-only; for a mutable borrow, it’s completely inaccessible. So there’s no way for the program to do anything that will invalidate the reference.
Paring these principles down to the simplest possible examples:
|
|
It is OK to reborrow a shared reference from a shared (the adjective “shared” speaks for itself) reference:
|
|
It’s OK to reborrow from a mutable (either mutable or shareable) reference:
|
|
Rust applies these rules everywhere: if we borrow, say, a shared reference to a key in a HashMap
, we can’t borrow a mutable reference to the HashMap
until the shared reference’s lifetime ends.
There’s good justification for this: designing collections to support unrestricted, simultaneous iteration and modification is difficult and often precludes simpler, more efficient implementations. Java’s Hashtable
and C++’s vector
don’t bother, and neither Python dictionaries nor JavaScript objects define exactly how such access behaves. Other collection types in JavaScript do, but require heavier implementations as a result. C++’s std::map
promises that inserting new entries doesn’t invalidate pointers to other entries in the map, but by making that promise, the standard precludes more cache-efficient designs like Rust’s BTreeMap
, which stores multiple entries in each node of the tree.
The following is another example of the kind of bug these rules catch. Consider the following C++ code, meant to manage a file descriptor:
|
|
If we assign a File
to itself, both rhs
and *this
are the same object, so operator=
closes the very file descriptor it’s about to pass to dup
. We destroy the same resource we were meant to copy.
The analogous code in Rust:
|
|
It turns out that two classic C++ bugs—failure to cope with self-assignment and using invalidated iterators—are the same underlying kind of bug! In both cases, code assumes it is modifying one value while consulting another, when in fact they’re both the same value. If you’ve ever accidentally let the source and destination of a call to memcpy
or strcpy
overlap in C or C++, that’s yet another form the bug can take. By requiring mutable access to be exclusive, Rust has fended off a wide class of everyday mistakes.
The immiscibility of shared and mutable references really demonstrates its value when writing concurrent code. A data race is possible only when some value is both mutable and shared between threads—which is exactly what Rust’s reference rules eliminate. A concurrent Rust program that avoids unsafe
code is free of data races by construction.
Rust’s Shared References Versus C’s Pointers to const
On first inspection, Rust’s shared references seem to closely resemble C and C++’s pointers to const values. However, Rust’s rules for shared references are much stricter:
|
|
The fact that p
is a const int *
means that you can’t modify its referent via p
itself: (*p)++
is forbidden. But you can also get at the referent directly as x
, which is not const
, and change its value that way. The C family’s const
keyword has its uses, but constant it is not.
In Rust, a shared reference forbids all modifications to its referent, until its lifetime ends:
|
|
To ensure a value is constant, we need to keep track of all possible paths to that value and make sure that they either don’t permit modification or cannot be used at all. C and C++ pointers are too unrestricted for the compiler to check this. Rust’s references are always tied to a particular lifetime, making it feasible to check them at compile time.
Taking Arms Against a Sea of Objects
Since the rise of automatic memory management in the 1990s, the default architecture of all programs has been the sea of objects.
This is what happens if you have garbage collection and you start writing a program without designing anything. This architecture has many advantages that don’t show up in the diagram: initial progress is rapid, it’s easy to hack stuff in, and a few years down the road, you’ll have no difficulty justifying a complete rewrite. (Cue AC/DC’s “Highway to Hell.”)
Of course, there are disadvantages too. When everything depends on everything else like this, it’s hard to test, evolve, or even think about any component in isolation.

One fascinating thing about Rust is that the ownership model puts a speed bump on the highway to hell. It takes a bit of effort to make a cycle in Rust—two values such that each one contains a reference pointing to the other. You have to use a smart pointer type, such as Rc
, and interior mutability. Rust prefers for pointers, ownership, and data flow to pass through the system in one direction.

The cure to a sea of objects is to do some up-front design and build a better program. Rust is all about transferring the pain of understanding your program from the future to the present. It works unreasonably well: not only can Rust force you to understand why your program is thread-safe, it can even require some amount of high-level architectural design.