- Traits: Defining Shared Behavior
- Some Built-in Traits
- Trait Objects
- Advanced Traits
- Choosing impl Trait or dyn Trait
A trait defines functionality a particular type has and can share with other types.
Known as interfaces feature in other languages.
Define a trait:
pub trait Summary {
// define shared method inside
fn summarize(&self) -> String;
}Implement Summary trait for some type: impl ... for ...
impl Summary for NewsArticle {
fn summarize(&self) -> String {
format!("{}, by {} ({})", self.headline, self.author, self.location)
}
}We can also give trait a default implementation
pub trait Summary {
fn summarize(&self) -> String {
String::from("(Read more...)")
}
}impl Summary for Tweet { }Default implementation can call other methods inside trait definition
pub trait Summary {
fn summarize_author(&self) -> String;
fn summarize(&self) -> String {
format!("(Read more from {}...)", self.summarize_author())
}
}example 1:
pub struct ReportCard<T: std::fmt::Display> {
pub grade: T,
pub student_name: String,
pub student_age: u8,
}
impl<T: std::fmt::Display> ReportCard<T> {
pub fn print(&self) -> String {
format!(
"{} ({}) - achieved a grade of {}",
&self.student_name, &self.student_age, &self.grade
)
}
}example 2:
trait Frobnicate<T> {
fn frobnicate(self) -> Option<T>;
}
impl<T> Frobnicate<T> for Foo<T> {
fn frobnicate(self) -> Option<T> {
Some(self.bar)
}
}
let another_foo = Foo { bar: 1 };
println!("{:?}", another_foo.frobnicate()); // Some(1)pub fn notify(item: &impl Summary) {
println!("Breaking news! {}", item.summarize());
}It is actually syntax sugar for a longer form, which is called a trait bound it looks like this:
pub fn notify<T: Summary>(item: &T) {
println!("Breaking news! {}", item.summarize());
}- this version of notify takes a reference to T.
- you can also use
item: impl Summary, orBox<impl Summary>
- you can also use
Trait bound is more clear when this function takes mutiple parameters with same type
pub fn notify<T: Summary>(item1: &T, item2: &T) {
// ...
}Note: If you want to use the type variable in multiple places you will need to use the longer version.
fn f(b1: impl Bar, b2: impl Bar) -> usizeis equivalent to
fn f<B1: Bar, B2: Bar>(b1: B1, b2: B2) -> usize
not
fn f<B: Bar>(b1: B, b2: B) -> usizepub fn notify(item: &impl Summary + Display) {The + syntax is also valid with trait bounds on generic types:
pub fn notify<T: Summary + Display>(item: &T) {Multiple trait bound could hinder readability.
fn some_function<T: Display + Clone, U: Clone + Debug>(t: T, u: U) -> i32 {We can rewrite the following code using where clause
fn some_function<T, U>(t: T, u: U) -> i32
where T: Display + Clone,
U: Clone + Debug
{impl Trait can also be used in the return type of a function. In this case it is not a shorthand for a generic type parameter, but a way to return a value of some type that implements a trait without naming the concrete type.
fn returns_summarizable() -> impl Summary {Returning types that implement a certain trait instead of concrete types is very useful inside of closures and iterators.
For example, the type Pair<T> always implements the new function.
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,
}
}
}But Pair<T> only implements the cmp_display method if its inner type T implements the PartialOrd trait that enables comparison and the Display trait that enables printing.
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);
}
}
}Blanket Implementation is an implement of a trait either for all types, or for all types that match some condition.
In this example, we implement the ToString trait on any type T that implements the Display trait.
impl<T: Display> ToString for T {
// --snip--
}if you use {:?} format to print a custom struct, rust will throw an error. To solve this problem, you need to add Debug trait to that struct.
#[derive(Debug)]
struct Person {
first_name: String,
last_name: String
}now you can use {:?} to print this struct.
if you use {} to print a struct, rust will ask you to implement std::fmt::Display trait.
use std::fmt;
impl fmt::Display for Person {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "({}, {})", self.first_name, self.last_name)
}
}#[derive(Clone)]
...
let a = Person {
first_name: String::from("John"),
last_name: String::from("Doe"),
};
let b = a.clone();
println!("{},{}", a, b);#[derive(Debug, Clone, Copy)]
struct Point {
x: i32,
y: i32,
}
fn main() {
let a = Point { x: 1, y: 2 };
let b = a;
println!("{:?},{:?}", a, b)
}Rust doesn't support classical inheritance. However it does achieve polymorphism, which is the ability for code to work on multiple types of data through trait objects.
An GUI app example:
lib.rs
pub trait Draw {
fn draw(&self);
}
// main screen
pub struct Screen {
// visual components to be drawn
pub components: Vec<Box<dyn Draw>>,
}
impl Screen {
// iterate over components and call draw on each
pub fn run(&self) {
for component in self.components.iter() {
component.draw();
}
}
}
// botton component
pub struct Button {
pub width: u32,
pub height: u32,
pub label: String,
}
impl Draw for Button {
fn draw(&self) {
println!("Drawing button with label {}", self.label);
}
}main.rs
use rust_tutor::{Button, Draw, Screen};
struct SelectBox {
width: u32,
height: u32,
options: Vec<String>,
}
impl Draw for SelectBox {
fn draw(&self) {
// Code to actually draw a select box
}
}
fn main() {
let screen = Screen {
components: vec![
Box::new(SelectBox {
width: 75,
height: 10,
options: vec![
String::from("Yes"),
String::from("Maybe"),
String::from("No"),
],
}),
Box::new(Button {
width: 50,
height: 10,
label: String::from("OK"),
}),
],
};
screen.run();
}You might noticed here we use dyn dynamic dispatch. The trait objects must include the dyn keyword, otherwise it it raise a compile error [E0782].
That is , a trait object is always passed by pointer (a borrowed reference, Box, or other smart pointer) and has a vtable so that methods can be dispatched dynamically.
The type of trait objects uses dyn Trait, e.g. &dyn Bar or Box<dyn Bar>.
Let's talk about a little bit about static vs dynamic dispatch.
Let's say if you had a function called add<T>(a:T, b:T) , you use that function with floating point numbers and integers. The compiler will generate a function called integer_add and a float_add , then it will find all the call sites of the add methods and replace it with the concrete implementation. This is called static dispatch.
The opposite is dynamic dispatch. Dynamic dispatch happens when the compiler does not know the concrete methods you're calling at compile time. Instead it figures that out at runtime. When using trait objects, the rust compiler must use dynamic dispatch. The compiler will add code to figure out the correct method to call at runtime.
You can only make object safety traits into trait bounds.
- A trait is object safe when all of the methods implemented on that trait have these 2 properties:
- the return type it not itself and there are no generic parameters
- if a trait does not have these 2 properties, then the rust compiler can't figure out the concrete type of that trait and therefore doesn't know the correct methods to call.
Associated Types are placeholders which you can add to your trait and then methods can use that placeholder.
pub trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
}For example here we've defined the Iterator trait which has one associated type named Item, and we use it in the next method. Then when we implement our iteractor trait we will specify a concrete type for Item.
This way you can define a trait which uses some type that's unknown until we implement the trait.
What's the difference between associated types and generics? They both allow use to define a type without specifying the concrete value. The difference is with associated types we can only have 1 concrete type per implementation.
struct Counter {}
// you implement Iterator trait with Item type u32
impl Iterator for Counter {
type Item = u32;
fn next(&mut self) -> Option<Self::Item> {
Some(1)
}
}
// you can NOT do another implementation with Item type u16, for examplewhereas with generics we can have multiple concrete types per implementation.
// Generic Example
pub trait Iterator<T> {
fn next(&mut self) -> Option<T>;
}
struct Counter {}
impl Iterator<u32> for Counter {
fn next(&mut self) -> Option<u32> {
Some(1)
}
}
impl Iterator<u16> for Counter {
fn next(&mut self) -> Option<u16> {
Some(1)
}
}Using associated type trait if for any given implementation you want the next method to return the same concrete type.
-
Advantages of
impl Traitor generics:- fine-grained control of properties of types using where clauses,
- can have multiple trait bounds (e.g.,
impl Foo + Quxis allowed, butdyn Foo + Quxis not),
-
Disadvantages of
impl Traitor generics:- monomorphisation causes increased code size.
-
Advantages of
dyn Trait:- a single variable, argument, or return value can take values of multiple different types.
-
Disadvantages of
dyn Trait:- virtual dispatch means slower method calls,
- objects must always be passed by pointer
- requires object safety