Traits in rust are similar to interfaces in other languages, but not quite the same.
defining a trait
A trait defines the functionality a particular type has and can share with other types. We can use traits to define shared behavior in an abstract way.
A trait allows us to describe data not by what it is, but by what it can do.
pub trait Summary {
fn summarize(&self) -> String;
}
pub struct NewsArticle {
pub headline: String,
pub location: String,
pub author: String,
pub content: String,
}
impl Summary for NewsArticle {
fn summarize(&self) -> String {
format!("{}, by {} ({})", self.headline, self.author, self.location)
}
}
pub struct Tweet {
pub username: String,
pub content: String,
pub reply: bool,
pub retweet: bool,
}
impl Summary for Tweet {
fn summarize(&self) -> String {
format!("{}: {}", self.username, self.content)
}
}
default implementations
Rust allows defining a default implementation of a trait, which is useful in a few specific cases:
pub trait Summary {
fn summarize(&self) -> String {
String::from("(Read more...)")
}
}
traits as parameters
We can use a trait as a parameter to a function or method and the caller will be able to provide any concrete type that implements the trait:
pub fn notify(item: &impl Summary) {
println!("Breaking news! {}", item.summarize());
}
The syntax above is actually just a shorthand for the trait bound syntax which uses rust generics:
pub fn notify<T: Summary>(item: &T) {
println!("Breaking news! {}", item.summarize());
}
multiple trait bounds
There are a few different syntaxes to choose from when we want to specify that a concrete type must implement several traits.
pub fn notify(item: &(impl Summary + Display)) {
pub fn notify<T: Summary + Display>(item: &T) {
fn some_function<T, U>(t: &T, u: &U) -> i32
where
T: Display + Clone,
U: Clone + Debug,
{
conditionally implement methods
By using a trait bound with an impl block that uses generic type parameters, we can implement methods conditionally for types that implement the specified traits.
use std::fmt::Display;
struct Pair<T> {
x: T,
y: T,
}
impl<T> Pair<T> {
fn new(x: T, y: T) -> Self {
Self { x, y }
}
}
impl<T: Display + PartialOrd> Pair<T> {
fn cmp_display(&self) {
if self.x >= self.y {
println!("The largest member is x = {}", self.x);
} else {
println!("The largest member is y = {}", self.y);
}
}
}
automatic implementation
Unfortunately, unlike go, we do not have traits implemented automatically as soon as a concrete type fulfills the interface. However, we can create traits which will become automatically implemented if other traits are implemented.
We can conditionally implement a trait for any type that implements another
trait. The standard library implements the ToString trait on any type that
implements the Display trait.
impl<T: Display> ToString for T {
// --snip--
}
Because the standard library has this blanket implementation, we can call the
to_string method defined by the ToString trait on any type that implements
the Display trait.
// Integers implement the Display trait so they automatically implement
// ToString.
let s = 3.to_string();