I’m accumulating notes on the principles in Rust. These are the things I want to keep in easy reach to refresh my understanding.
Sources
- The Rust Programming Language, Steve Klabnik and Carol Nichols, with contributions from the Rust Community.
- Rust by Example.
- Rust in Motion, Carol Nichols and Jake Goulding, Manning liveVideo.
- The Rust Reference.
- Rust RFCs.
- The Rustonomicon.
Ownership rules
Each value in Rust has a variable that’s called its owner. There can only be one owner at a time. When the owner goes out of scope, the value will be dropped. 1
It’s more than memory management: it applies to sockets and locks, for example.
Borrowing
Most of the time, we’d like to access data without taking ownership over it. To accomplish this, Rust uses a borrowing mechanism. Instead of passing objects by value (
T
), objects can be passed by reference (&T
). 3
References
Consider: &foo
or &mut foo
.
These ampersands are references, and they allow you to refer to some value without taking ownership of it.
At any given time, you can have either one mutable reference or any number of immutable references. 2
Use *
to dereference a reference 9.
Implicit deref coercions
Implementing the
Deref
trait allows you to customize the behavior of the dereference operator,*
6
From 7:
Deref coercion is a convenience that Rust performs on arguments to functions and methods.
Deref
coercion converts a reference to a type that implementsDeref
into a reference to a type thatDeref
can convert the original type into.
Deref
coercion happens automatically when we pass a reference to a particular type’s value as an argument to a function or method that doesn’t match the parameter type in the function or method definition.
A sequence of calls to the
deref
method converts the type we provided into the type the parameter needs.
There are other coercions:
They mostly exist to make Rust “just work” in more cases, and are largely harmless. 8
Lifetime elision
From my notes from Rust in Motion:
A lifetime is the time during which a value is at a particular memory location. Generic lifetimes are needed so the compiler can prove references are valid for some concrete lifetime.
The three lifetime elision rules mean you often do not need to explicitly state the lifetime. That is, we can omit lifetimes for common cases based on a type signature (not code body).
From 4:
The first rule is that each parameter that is a reference gets its own lifetime parameter […]
The second rule is if there is exactly one input lifetime parameter, that lifetime is assigned to all output lifetime parameters […]
The third rule is if there are multiple input lifetime parameters, but one of them is
&self
or&mut self
because this is a method, the lifetime ofself
is assigned to all output lifetime parameters.
Argument dereferencing pattern
Patterns destructure values.
When pattern matching, &x
is dereferencing (a mirror of the way a value is constructed).
Match binding mode
…patterns operate in different binding modes in order to make it easier to bind references to values. When a reference value is matched by a non-reference pattern, it will be automatically treated as a
ref
orref mut
binding. 10
Default binding mode: this mode, either
move
,ref
, orref mut
, is used to determine how to bind new pattern variables. When the compiler sees a variable binding not explicitly markedref
,ref mut
, ormut
, it uses the default binding mode to determine how the variable should be bound. Currently, the default binding mode is alwaysmove
. Under this RFC, matching a reference with a non-reference pattern, would shift the default binding mode toref
orref mut
. 11
For more examples, see Ivan Veselov’s post on More advanced aspects of pattern matching in Rust.
Dynamic dispatch
The dyn
keyboard signifies a type that uses dynamic dispatch.
Trait objects
Trait objects as parameters support dynamic dispatch.
A trait object is an opaque value of another type that implements a set of traits. The set of traits is made up of an object safe base trait plus any number of auto traits. 17
To be object-safe, a trait must:
- not return type
Self
; and - have no generic type parameters.
Fn
and fn
fn
is a function pointer.Fn
is a trait.
Closures
A closure is:
a unique, anonymous type that cannot be written out. A closure type is approximately equivalent to a struct which contains the captured variables.
The compiler prefers to capture a closed-over variable by immutable borrow, followed by unique immutable borrow [a closure-specific feature], by mutable borrow, and finally by move. It will pick the first choice of these that allows the closure to compile.
From 16.
move
If you want to force the closure to take ownership of the values it uses in the environment, you can use the
move
keyword before the parameter list. This technique is mostly useful when passing a closure to a new thread to move the data so it’s owned by the new thread. 12
Smart pointers
Smart pointers, on the other hand, are data structures that not only act like a pointer but also have additional metadata and capabilities. 5
Examples include String
, Vec<T>
. They are smart in that they own the memory they point at, and provide other capabilities such as knowing their length
Use Box<T>
for values allocated on the heap:
Box<T>
is a a fixed size, and so can live on the stack.- A good use case is for transferring ownership without copying the (potentially large) data pointed at.
- Another use case is for recursive data structures.
Slices
Slices let you reference a contiguous sequence of elements in a collection rather than the whole collection. 13
The type is: &[T]
.
Strings
Via 14:
String
is a growable, owned, heap-allocated UTF-8 encoded string.&String
is a reference to aString
str
is a string slice, the type of a"string literal"
.&str
is a borrowed string slice.
The empty type
It is: !
, called “Never” 15.