[Rust] Trait
A trait defines functionality a particular type has and can share with other types.
We can use traits to define shared behavior in an abstract way.
We can use trait bounds to specify that a generic type can be any type that has certain behavior.
Define Trait
Trait definitions are a way to group method signatures together to define a set of behaviors necessary to accomplish some purpose.
Traits can be declared using the trait keyword and the trait's name.
pub trait Summary {
fn summarize(&self) -> String;
}
We've also declared the trait as pub so that crates depending on this crate can make use of this trait too.
Inside the curly brackets, we declare the method signatures that describe the behaviors of the types that implement this trait.
After the method signature, instead of providing an implementation within curly brackets, we use a semicolon.
A trait can have multiple methods in its body.
Implement Trait
Implementing a trait on a type is similar to implementing regular methods.
pub struct Tweet {
pub username: String,
pub content: String,
}
impl Summary for Tweet {
fn summarize(&self) -> String {
format!("{}: {}", self.username, self.content)
}
}
fn main() {
let tweet = Tweet {
username: String::from("HoYa"),
content: String::from("Melong!"),
};
println!("{}", tweet.summarize());
}
The difference is that after impl, we put the trait name we want to implement, then use the for keyword, and then specify the name of the type we want to implement the trait for.
Default Implementation
Sometimes it's useful to have default behavior for some or all of the methods in a trait instead of requiring implementations for all methods of every type.
Then, as we implement the trait on a particular type, we can keep or override each method's default behavior.
pub trait Summary {
fn summarize(&self) -> String {
String::from("(more...)")
}
}
pub struct News {
pub title: String,
pub content: String,
}
impl Summary for News { }
fn main() {
let news = News {
title: String::from("Headline"),
content: String::from("Blah blah"),
};
println!("{}", news.summarize());
}
Default implementations can call other methods in the same trait, even if those other methods don't have a default implementation.
Traits as Parameters
To define a function with traits as parameters, we use the impl Trait syntax.
pub fn notify(item: &impl Summary) {
println!("Breaking news! {}", item.summarize());
}
fn main() {
let tweet = Tweet {
username: String::from("HoYa"),
content: String::from("Melong!"),
};
notify(&tweet);
}
The item parameter accepts any type that implements the specified trait.
Trait Bound Syntax
The impl Trait syntax works for straightforward cases but is actually syntax sugar for a longer form known as a trait bound.
pub fn notify<T: Summary>(item: &T) { ... }
We can easily extend using this syntax.
pub fn notify<T: Summary>(item1: &T, item2: &T) { ... }
Multiple Trait Bounds
We can also specify more than one trait bound using + syntax.
pub fn notify(item: &(impl Summary + Display)) { ... }
pub fn notify<T: Summary + Display>(item: &T) { ... }
Trait Bounds with where Clauses
Lots of trait bounds make the function signature hard to read.
where clauses make it easy.
// Before
fn func<T: Display + Clone, U: Clone + Debug>(t: &T, u: &U) -> i32 { ... }
// After
fn func<T, U>(t: &T, u: &U) -> i32
where T: Display + Clone,
U: Clone + Debug
{ ... }
Traits as Return Type
We can use impl Trait syntax in the return position to return a value of some type that implements a trait.
fn summarizable() -> impl Summary {
Tweet {
username: String::from("HoYa"),
content: String::from("Melong"),
}
}
Trait Bounds to 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);
}
}
}
fn main() {
let pair = Pair::new(3, 4);
pair.cmp_display();
}