Traits
In the previous session we implemented some functions on a struct
. In Rust these functions can be grouped together
to form a so-called trait
. Like with humans, a trait
is a combination of characteristics that all types that
have that trait
expose, let's say implement.
trait Fruitiness { fn is_sweet(&self) -> bool; } struct Pear {} struct Lemon {} impl Fruitiness for Pear { fn is_sweet(&self) -> bool { true } } impl Fruitiness for Lemon { fn is_sweet(&self) -> bool { false } } fn print_sweetness(id: &str, fruit: impl Fruitiness) { println!("{} is sweet? {}", id, fruit.is_sweet()); } fn main() { let pear = Pear {}; let lemon = Lemon {}; print_sweetness("pear", pear); print_sweetness("lemon", lemon); }
Traits can have default implementations that are available to all types that implement that trait
. The default
implementation can be overridden when desired.
trait Fruitiness { fn is_sweet(&self) -> bool { self.sweetness() >= 0.5 } fn sweetness(&self) -> f32; } struct Pear {} struct Lemon {} impl Fruitiness for Pear { fn sweetness(&self) -> f32 { 0.6 } } impl Fruitiness for Lemon { fn sweetness(&self) -> f32 { 0.2 } } fn print_sweetness(id: &str, fruit: impl Fruitiness) { println!("{} is sweet? {}", id, fruit.is_sweet()); } fn main() { let pear = Pear {}; let lemon = Lemon {}; print_sweetness("pear", pear); print_sweetness("lemon", lemon); }
Traits are used throughout Rust, and often types implement a combination of traits, like the Display
trait we
discussed right at the beginning of this course.
use std::fmt::{Display, Formatter, Result}; trait Fruitiness { fn is_sweet(&self) -> bool { self.sweetness() >= 0.5 } fn sweetness(&self) -> f32; } struct Pear {} struct Lemon {} impl Fruitiness for Pear { fn sweetness(&self) -> f32 { 0.6 } } impl Display for Pear { fn fmt(&self, f: &mut Formatter<'_>) -> Result { f.write_str("A pear") } } impl Fruitiness for Lemon { fn sweetness(&self) -> f32 { 0.2 } } impl Display for Lemon { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { f.write_str("A lemon") } } fn print_sweetness(fruit: impl Fruitiness + Display) { println!("{} is sweet? {}", fruit, fruit.is_sweet()); } fn main() { let pear = Pear {}; let lemon = Lemon {}; print_sweetness(pear); print_sweetness(lemon); }
The signature of the Display
trait is common to all objects in Rust. RustRover will actually scaffold the
implementation for you the moment you type: impl Display for Pear {}
. Use the alt + Enter
keyboard combination while
the cursor is on the (red underlined) line. Then pick "Implement members."
If you are more "mouse" oriented, you can also click on the red (or yellow) light bulb and pick the same option from the list.
Notice the "use" statements at the top of our code. This is Rust's way of importing external objects and traits into our application. Often these are automatically added by the IDE. You can use the same
alt + Enter
combination to add missing imports.
You can see that traits can be combined using the +
operator. If you need a combination of many traits you can use a
type signature to make you code more readable.
use std::fmt::{Debug, Display, Formatter, Result}; trait Fruitiness { fn is_sweet(&self) -> bool { self.sweetness() >= 0.5 } fn sweetness(&self) -> f32; } struct Pear {} struct Lemon {} impl Fruitiness for Pear { fn sweetness(&self) -> f32 { 0.6 } } impl Display for Pear { fn fmt(&self, f: &mut Formatter<'_>) -> Result { f.write_str("A pear") } } impl Debug for Pear { fn fmt(&self, f: &mut Formatter<'_>) -> Result { f.write_str("A debugged pear") } } impl Fruitiness for Lemon { fn sweetness(&self) -> f32 { 0.2 } } impl Display for Lemon { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { f.write_str("A lemon") } } impl Debug for Lemon { fn fmt(&self, f: &mut Formatter<'_>) -> Result { f.write_str("A debugged lemon") } } fn print_sweetness<T>(fruit: T) where T: Fruitiness + Display + Debug, { println!("{} is sweet? {}", fruit, fruit.is_sweet()); } fn main() { let pear = Pear {}; let lemon = Lemon {}; print_sweetness(pear); print_sweetness(lemon); }
Without going into detail right now on how the mechanism actually works, I do want to point out that common traits can
often be derived automatically by Rust. For example, the Debug
trait in the above code, can be replaced with a
derived implementation. It saves a few lines of code!
use std::fmt::{Debug, Display, Formatter, Result}; trait Fruitiness { fn is_sweet(&self) -> bool { self.sweetness() >= 0.5 } fn sweetness(&self) -> f32; } #[derive(Debug)] struct Pear {} #[derive(Debug)] struct Lemon {} impl Fruitiness for Pear { fn sweetness(&self) -> f32 { 0.6 } } impl Display for Pear { fn fmt(&self, f: &mut Formatter<'_>) -> Result { f.write_str("A pear") } } impl Fruitiness for Lemon { fn sweetness(&self) -> f32 { 0.2 } } impl Display for Lemon { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { f.write_str("A lemon") } } fn print_sweetness<T>(fruit: T) where T: Fruitiness + Display + Debug, { println!("{:?} is sweet? {}", fruit, fruit.is_sweet()); } fn main() { let pear = Pear {}; let lemon = Lemon {}; print_sweetness(pear); print_sweetness(lemon); }