I’m accumulating notes on the principles in Rust. These are the things I want to keep in easy reach to refresh my understanding.
- Ownership rules
- Implicit deref coercions
- Lifetime elision
- Argument dereferencing pattern
- Match binding mode
- Dynamic dispatch
- Trait objects
- Smart pointers
- The empty type
- 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.
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.
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 (
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
* to dereference a reference 9.
Implicit deref coercions
Dereftrait allows you to customize the behavior of the dereference operator,
Deref coercion is a convenience that Rust performs on arguments to functions and methods.
Derefcoercion converts a reference to a type that implements
Derefinto a reference to a type that
Derefcan convert the original type into.
Derefcoercion 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
derefmethod 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
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).
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
&mut selfbecause this is a method, the lifetime of
selfis 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 mutbinding. 10
Default binding mode: this mode, either
ref mut, is used to determine how to bind new pattern variables. When the compiler sees a variable binding not explicitly marked
ref mut, or
mut, it uses the default binding mode to determine how the variable should be bound. Currently, the default binding mode is always
move. Under this RFC, matching a reference with a non-reference pattern, would shift the default binding mode to
ref mut. 11
For more examples, see Ivan Veselov’s post on More advanced aspects of pattern matching in Rust.
dyn keyboard signifies a type that uses dynamic dispatch.
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
- have no generic type parameters.
fnis a function pointer.
Fnis a trait.
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.
If you want to force the closure to take ownership of the values it uses in the environment, you can use the
movekeyword 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, on the other hand, are data structures that not only act like a pointer but also have additional metadata and capabilities. 5
Vec<T>. They are smart in that they own the memory they point at, and provide other capabilities such as knowing their length
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 let you reference a contiguous sequence of elements in a collection rather than the whole collection. 13
The type is:
Stringis a growable, owned, heap-allocated UTF-8 encoded string.
&Stringis a reference to a
stris a string slice, the type of a
&stris a borrowed string slice.
The empty type
!, called “Never” 15.