Traits
A trait is analogous to an interface or protocol from other languages. It enables types to advertise that they use some common behaviour (methods).
All of Rust’s operations are defined with traits. E.g., aaddition (+
) is
defined as the std::ops::Add
trait. Operators are just syntactic sugar for
traits’ methods.
a + b
= a.add(b)
Defining a Trait
Here’s an example of a trait that contains a summarize
method. Any type that
has this trait needs to implement such a method.
pub trait Summary {
fn summarize(&self) -> String;
}
We can also provide a default implementation:
pub trait Summary {
fn summarize(&self) -> String {
String::from("(Read more...)")
}
}
Implementing Traits
A type may implement a trait as follows:
// type itself
pub struct Tweet {
pub username: String,
pub content: String,
pub reply: bool,
pub retweet: bool,
}
//Summary trait implementation
impl Summary for Tweet {
fn summarize(&self) -> String {
format!("{}: {}", self.username, self.content)
}
}
To use a trait that has some default implementation (without overwriting it), we can do it as follows:
impl Summary for Tweet {}
Or we could overwrite it, the same way as implementing a trait.
Blanket Implementations
We can define methods for any type that implements some trait. It is similiar to extensions in C#.
Rust does it with the ToString
method, like this:
impl<T: Display> ToString for T {
// ...
}
Thanks to it, any type that implement the Display
trait has the ToString
method
Using Traits
Function Parameter
Here’s a function that expects any object that implements a Summary
trait:
pub fn notify(item: &impl Summary) {
println!("Breaking news! {}", item.summarize());
}
or a full generic form:
pub fn notify<T: Summary>(item: &T) {
println!("Breaking news! {}", item.summarize());
}
Function Return Value
Functions can return types and specify just the trait of these types:
fn returns_summarizable() -> impl Summary {
// return anything that implements Summary
}
Derivable Traits
Some traits (in the standard library and third party) have sensible default
implementations and they can be implemented on a type just by adding an
annotation #[derive(SomeTrait)]
.
Here are the ones from the standard library:
Debug
Writing #[derive(Debug)]
before struct’s definition makes that struct
printable in debug mode (print!("{}", instance)
).
Another way to debug print is with the use of dbg!(&instance)
.
PartialEq and Eq
PartialEq
allows checking equality with the ==
and !=
operators.
Underneath there’s just the eq
method. The default implementation checks all
fields of a struct if they’re equal.
The Eq
trait has no methods.
PartialOrd and Ord
Allows comparisons with the >
, <
, >=
, <=
operators. It can only be
applied to types that implement PartialEq
as well.
In structs, all fields are checked.
Clone
Allows creation of deep copy of a value. Default implementation calls clone()
on each field of the type. Cloning might involve copying heap data.
Copy
Allows copying a value on a stack. All types that implement Copy
must also
implement Clone
.
Hash
Allows creating some hash of an instance. The default implementation combines
resuts of hash()
of all the fields of a struct.
Default
Allows to create a default value for a type. It provides a default()
function.
Trait Objects
Trait Objects enable polymorphism.
Without trait objects:
pub struct Screen<T: Draw> {
pub components: Vec<T>,
}
The components
vector’s items must all be of the same type. If we want to have
a vector that may contain values of any type (that implements Draw
), we can
use trait object:
pub struct Screen {
pub components: Vec<Box<dyn Draw>> // Box<dyn Draw> is a trait object
}
Now, we could apply the following code on top of that to make use of polymorphism:
impl Screen {
pub fn run(&self) {
for component in self.components.iter() {
component.draw();
}
}
}
This feature is a bit similar to what we can do in languages like JS (duck typing), but different in the sense that the existence of required methods is not done during runtime, and the code cannot panic due to some value not implementing a required trait. It’s safer. However, Rust still needs to perform dynamic dispatch to find the code of the method on values in runtime. This incurs some cost.
Trait objects may be used if in trait’s methods:
- The return type isn’t
Self
. - There are no generic type parameters.
Associated Types
Associated types allow traits to act a bit as if they were generic.
A real example from the standard library:
pub trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
}
The next
method will return Option<Item>
and the Item
type is unknown
in the trait’s definition. The types that increment Iterator
have to specify
what Item
stands for:
impl Iterator for Counter {
type Item = u32; // concrete type
fn next(&mut self) -> Option<Self::Item> { ... }
}
Supertraits
Some traits might require other traits also to be implemented by the types that want to use it.
use std::fmt;
trait OutlinePrint: fmt::Display {
fn outline_print(&self) {
let output = self.to_string(); // comes from Display
let len = output.len();
println!("{}", "*".repeat(len + 4));
println!("*{}*", " ".repeat(len + 2));
println!("* {} *", output);
println!("*{}*", " ".repeat(len + 2));
println!("{}", "*".repeat(len + 4));
}
}
In the example above, Display
is the supertrait of OutlinePrint
.