Rust¶
“the book” from is a good introduction and contains examples.
Key features of Rust:
Basic memory handling¶
Rust doesn’t use a garbage collector; values are “dropped” automatically when they go out of scope.
References can’t outlive the value they are referencing; this is checked by statically tracking lifetimes of values and references.
Lifetimes and ownership errors are what beginners struggle with often - lifetime problems can often be solved by cloning values.
Rust distinguishes between copyable “primitive” data (Rust automatically copies instead of moving for those types) and cloning complex data (which requires an explicit .clone()).
Ownership¶
Rust tracks references to objects (as in “any stored data”) to make sure an object can’t be modified while other references exist; that also ensures the referenced object isn’t freed in the meantime.
Ownership is usually checked at compile time, but it can be moved to runtime by using std::sync::Mutex or similar constructs.
Thread safety¶
In addition to tracking ownership non-thread-safe data can’t be shared across threads, based on the Sync and Send traits.
Error handling¶
While there is unwinding as part of panic! handling, this is not used for normal error handling (not used as part of an exception mechanism).
Instead functions that can fail return a Result, carrying either a value wrapped in Ok or an error wrapped in Err.
Error handling often consists of applying the ? operator to an Result expression, which unpacks the value if the result contains Ok, or returns the error from Err.
Destructive move and pinning¶
All values in Rust can be moved; they are always moved by copying the bytes making up the value. No “move constructor” is called on the new location, and no destructor run on the old location.
You can’t move a value out of a (mutable) reference, although “nullable” types like Option often provide a take() method to extract a contained value.
This means a value can’t easily contain references to itself. The (rather complex) solution to this is pinning: the basic idea is to promise through the type system a value is never going to be moved by using Pin<&mut T> - similar to a normal mutable reference, but with the promise that the referenced value is not moving anymore.
There are two ways to safely “pin” a value: put it into a heap allocation (Pin<Box<T>>) or stick it to the local scope (stack) with pin!: this ensures a variable is moved to the local scope, and then prevents anything from ever accessing it by shadowing it with the reference to it.
Type system¶
There are two main ways to define own data types; structs und enums. The types itself then offer low-level constructors to create a value those types. Often those are private to the module defining the type, and the type itself offers other ways (e.g. “named constructors”) to build values.
Structs¶
There are three struct types; a unit struct (struct Unit;), carrying no data, a “named tuple” struct (struct Tuple(i32, u32);), using indices instead of fieldnames, and “normal” structs with field names (struct Struct { name: String, data: u32 }).
enum / tagged unions¶
Enums define a list of variants; a value can contain exactly one variant at a time. Each variant is basically a struct definition - so in addition to storing the variant, it also stores data associated with a variant.
enum Result<T, E> { Ok(T), Err(E) } is an example of an enum with variants containing data.
As a special case one can define an enum without any variants like Infallible.
Traits¶
Traits are similar to “interfaces” from other languages; they define how a type that implements the trait can be interacted with by specifying functions and “associated” types.
Trait implementations are explicit, and have their own namespace - two traits containing a method with the same name (regardless of the signature) can be implemented for the same type, because each implementation is separate.
Traits that only define methods with certain restrictions can be used for dynamic dispatch (in the form of &dyn Trait references or heap allocations like Box<dyn Trait>); otherwise they are just used for generics (static dispatch, monomorphization).
Generics¶
Functions and types can use generic type parameters. Certain trait implementations can be listed as requirement. Contrary to C++ only listed traits can be used, otherwise generics are “transparent”.
Additionally to type parameters constants can be passed as generics (e.g. array sizes), but what is accepted as value to those constant parameters is very limited.