Rust principles
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
Dereftrait 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.
Derefcoercion converts a reference to a type that implementsDerefinto a reference to a type thatDerefcan 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
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
&selfor&mut selfbecause this is a method, the lifetime ofselfis 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
reforref mutbinding. 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 toreforref 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
-
fnis a function pointer. -
Fnis 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
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
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:
-
Stringis a growable, owned, heap-allocated UTF-8 encoded string. -
&Stringis a reference to aString -
stris a string slice, the type of a"string literal". -
&stris a borrowed string slice.
The empty type
It is: !, called “Never” 15.