- When trait
A
requires traitB
to also be implemented in order to function, then traitB
is called a supertrait. - Example:
use std::fmt; trait OutlinePrint: fmt::Display { fn outline_print(&self) { // `to_string` method defined in `Display` trait let output = self.to_string(); let len = output.len(); println!("{}", "*".repeat(len + 4)); println!("*{}*", " ".repeat(len + 2)); println!("* {} *", output); println!("*{}*", " ".repeat(len + 2)); println!("{}", "*".repeat(len + 4)); } }
- Trying to implement
OutlinePrint
trait forPoint
struct:will result in the following error:struct Point { x: i32, y: i32, } impl OutlinePrint for Point {}
That's correct!error[E0277]: `Point` doesn't implement `std::fmt::Display`
Point
doesn't implementDisplay
trait. - Let's fix the above error by implementing
Display
trait forPoint
:use std::fmt; impl fmt::Display for Point { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "({}, {})", self.x, self.y) } }
- Now, running
point.outline_print()
, wherepoint
is an instance ofPoint
struct will give the following output:********** * * * (1, 3) * * * **********
- In Rust, in order to implement a trait for a type, either the trait or the type should be local to our crate.
- Using newtype pattern, we can implement external traits on external types.
- This involves creating a new type in a tuple struct.
- The tuple struct will have one field and be a thin wrapper around the type we want to implement a trait for.
- Then the wrapper type is local to our crate, and we can implement the trait on the wrapper.
- Newtype is a term that originates from the Haskell programming language.
- There is no runtime performance penalty for using this pattern
- Example,
Point
struct andDistance
trait are external to our crate and are defined indas
crate. We can implementDistance
trait forPoint
struct as follows:Output:// das crate pub struct Point { x: u32, y: u32 } pub trait Distance { pub distance_from_origin(&self) -> f64; } // local crate use das::{Point, Distance}; use std::num::sqrt; struct Wrapper(Point); impl Distance for Wrapper { pub distance_from_origin(&self) -> f64 { let sum = (self.0.x * self.0.x) + (self.0.y * self.0.y); (sum as f64).sqrt() } } fn main() { let wrapped_point = Wrapper(Point{x: 3, y: 4}); println!("Distance from origin: {}", wrapped_point.distance_from_origin()); }
Distance from origin: 5.0
- Disadvantage:
Wrapper
is a new type, so it doesn’t have the methods of the value it’s holding (here,Point
's methods are absent fromwrapped_point
). - For new type to have all methods of the inner type, implement
Deref
trait on theWrapper
to return the inner type. (TODO: example code) - If
Wrapper
type need to have select methods of inner type, we would have to manually implement them.
- Suppose we have the following trait
Draw
:pub trait Draw { fn draw(&self); }
- We have two structs
Button
andSelectBox
implementingDraw
trait:pub struct Button { pub label: String, } impl Draw for Button { fn draw(&self) { // code to actually draw a button } } struct SelectBox { options: Vec<String>, } impl Draw for SelectBox { fn draw(&self) { // code to actually draw a select box } }
- Now, let's say we want to define a struct
Screen
storing instances of types implementingDraw
trait in a field calledcomponents
.- This way, we can store instances of
Button
andSelectBox
incomponents
field. - We can't define
Screen
as follows:because a generic type parameter can only be substituted with one concrete type at a time.pub struct Screen<T: Draw> { pub components: Vec<T>, }
- This way, we can store instances of
- In order to allow for multiple concrete types, we use trait objects.
- Trait objects allow for multiple concrete types to fill in for the trait object at runtime.
- They are defined as
Box<dyn TRAIT_NAME>
, whereTRAIT_NAME
represents name of the trait the concrete types implement.
- They are defined as
- So, using trait objects, we can defined
Screen
struct as follows:pub struct Screen { pub components: Vec<Box<dyn Draw>>, }
- Now, we can store instances of
Button
andSelectBox
incomponents
field ofScreen
struct as follows:fn main() { let screen = Screen { components: vec![ Box::new(SelectBox { options: vec![ String::from("Yes"), String::from("Maybe"), String::from("No"), ], }), Box::new(Button { label: String::from("OK"), }), ], }; screen.run(); }
- If you try to add a instance of a type which does not implement
Draw
trait tocomponents
field, you will get a compile-time error.
- The code that results from monomorphization (discussed here) is doing static dispatch, which is when the compiler knows what method you’re calling at compile time.
- When we use trait objects, Rust uses dynamic dispatch.
- A dynamic dispatch is when the compiler can’t tell at compile time which method you’re calling.
- In dynamic dispatch cases, the compiler emits code that at runtime will figure out which method to call using the pointers inside the trait object.
- There is a runtime cost with such lookups that doesn’t occur with static dispatch.
- You can only make object-safe traits into trait objects.
- A trait is object safe if all the methods defined in the trait have the following properties:
- The return type is not
Self
.- The
Self
keyword is an alias for the concret type. - If a trait method returns the concrete
Self
type, but a trait object forgets the exact type thatSelf
is, there is no way the method can use the original concrete type.
- The
- There are no generic type parameters.
- As we know, the generic type parameters are filled in with concrete type parameters when the trait is used.
- This way, the concrete types become part of the type that implements the trait.
- When the type is forgotten through the use of a trait object, there is no way to know what types to fill in the generic type parameters with.
- The return type is not
- For example, the following code will result in compile-time error:
Error:
pub trait Clone { fn clone(&self) -> Self; } pub struct Screen { pub components: Vec<Box<dyn Clone>>, }
error[E0038]: the trait `Clone` cannot be made into an object