Bounds When working with generics, the type parameters often must use traits as bounds to stipulate what functionality a type implements. For example, the following example uses the trait Display to print and so it requires T to be bound by...
Testcase: empty bounds A consequence of how bounds work is that even if a trait doesn't include any functionality, you can still use it as a bound. Eq and Copy are examples of such traits from the std library. struct Cardinal; struct BlueJa...
Multiple bounds Multiple bounds for a single type can be applied with a +. Like normal, different types are separated with ,. use std::fmt::{Debug, Display}; fn compare_prints<T: Debug + Display>(t: &T) { println!("Debug: `{:?}`", t); print...
Where clauses A bound can also be expressed using a where clause immediately before the opening {, rather than at the type's first mention. Additionally, where clauses can apply bounds to arbitrary types, rather than just to type parameters...
New Type Idiom The newtype idiom gives compile time guarantees that the right type of value is supplied to a program. For example, a function that measures distance in miles, must be given a value of type Miles. struct Miles(f64); struct Ki...
Associated items "Associated Items" refers to a set of rules pertaining to items of various types. It is an extension to trait generics, and allows traits to internally define new items. One such item is called an associated type, providing...
The Problem A trait that is generic over its container type has type specification requirements - users of the trait must specify all of its generic types. In the example below, the Contains trait allows the use of the generic types A and B...
Associated types The use of "Associated types" improves the overall readability of code by moving inner types locally into a trait as output types. Syntax for the trait definition is as follows: // `A` and `B` are defined in the trait via t...
Phantom type parameters A phantom type parameter is one that doesn't show up at runtime, but is checked statically (and only) at compile time. Data types can use extra generic type parameters to act as markers or to perform type checking at...
Testcase: unit clarification A useful method of unit conversions can be examined by implementing Add with a phantom type parameter. The Add trait is examined below: // This construction would impose: `Self + RHS = Output` // where RHS defau...
Scoping rules Scopes play an important part in ownership, borrowing, and lifetimes. That is, they indicate to the compiler when borrows are valid, when resources can be freed, and when variables are created or destroyed.
RAII Variables in Rust do more than just hold data in the stack: they also own resources, e.g. Box<T> owns memory in the heap. Rust enforces RAII (Resource Acquisition Is Initialization), so whenever an object goes out of scope, its destruc...
Ownership and moves Because variables are in charge of freeing their own resources, resources can only have one owner. This prevents resources from being freed more than once. Note that not all variables own resources (e.g. references). Whe...
Mutability Mutability of data can be changed when ownership is transferred. fn main() { let immutable_box = Box::new(5u32); println!("immutable_box contains {}", immutable_box); // Mutability error //*immutable_box = 4; // *Move* the box, c...
Partial moves Within the destructuring of a single variable, both by-move and by-reference pattern bindings can be used at the same time. Doing this will result in a partial move of the variable, which means that parts of the variable will...
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). The compiler stati...
Mutability Mutable data can be mutably borrowed using &mut T. This is called a mutable reference and gives read/write access to the borrower. In contrast, &T borrows the data via an immutable reference, and the borrower can read the data bu...
Aliasing Data can be immutably borrowed any number of times, but while immutably borrowed, the original data can't be mutably borrowed. On the other hand, only one mutable borrow is allowed at a time. The original data can be borrowed again...
The ref pattern When doing pattern matching or destructuring via the let binding, the ref keyword can be used to take references to the fields of a struct/tuple. The example below shows a few instances where this can be useful: #[derive(Clo...
Lifetimes A lifetime is a construct the compiler (or more specifically, its borrow checker) uses to ensure all borrows are valid. Specifically, a variable's lifetime begins when it is created and ends when it is destroyed. While lifetimes a...