September 15, 2020
The Rust programming language has some interesting traits tucked away in its standard library that are often used to implement common patterns and abstractions seen in other programming languages.
In this post I want to cover and provide examples for some of the traits which I find to be the most useful in my experience.
The Default trait provides a type with a default constructor.
Here is an example describing a Theme enum who's default variant is System:
enum Theme { Light, Dark, System } impl Default for Theme { fn default() -> Self { Self::System } }
Theme::System can now be constructed like so:
Theme::default()
The From trait allows for a type T to be infallibly converted to a seperate type, U.
More specifically, implementing From<T> for a type U allows for U to be constructed from a T by calling U::from.
Consider the following example:
struct Point { x: i32, y: i32 } impl From<(i32, i32)> for Point { fn from(tuple: (i32, i32)) -> Self { Self { x: tuple.0, y: tuple.1 } } }
Here we implement From for Point so that we can construct a Point from a (i32, i32).
Point::from((0, 0))
For fallible conversions, TryFrom can be used instead.
The Into trait is a reciprical version of From.
If the From trait allows for a type T to be converted to a seperate type U by calling U::from, then the Into trait allows for a type T to be converted to a U by calling into on a T.
If a type T implements From<U>, then an implemenation of Into<T> is provided for U automatically.
Because of this, one should always choose to implement From over Into if possible.
Since we implemented From<(i32, i32)> for Point in our previous example, we are provided with an implemenation of Into<Point> for (i32, i32)
Recall how we were able to construct a Point from a (i32, i32).
Point::from((0, 0))
This same Point construction could've been achieved using the Into trait. The alternative code for which goes as follows:
(0, 0).into()
The Deref trait is used for overloading the dereference operator (*).
This trait is often implemented for types which act as smart-pointers, allowing for implicit dereferences to data which the pointer is pointing to.
For example, let's say that we have the following struct:
struct Container<T> { data: T }
And we would like for any instance of Container<T> to implicity coerce to a &T whenever necessary. An implementation of Deref<Target = T> for Container<T> would enable this type of behaviour, and goes as follows:
impl<T> Deref for Container<T> { type Target = T; fn deref(&self) -> &T { &self.data } }
Due to this implementation, the following snippet of code is now valid:
let container = Container::<i32> { data: 0 }; // Dereferencing let x: &i32 = &container; // (Implicit) let y: &i32 = &*container; // * (Explicit #1) let z: &i32 = &(container.deref()); // .deref() (Explicit #2 // Implicit dereferencing for method calls container.pow(2); // Calls i32::pow