Traits: Defining Shared Behavior
About This Module
This module introduces Rust's trait system, which allows you to define shared behavior that can be implemented by different types. Traits are similar to interfaces in other languages but more powerful, enabling polymorphism, generic programming, and code reuse while maintaining Rust's safety guarantees.
Prework
Prework Reading
Please read the following sections from The Rust Programming Language Book:
- Chapter 10.2: Traits: Defining Shared Behavior
- Chapter 17.2: Using Trait Objects That Allow for Values of Different Types
- Chapter 19.3: Advanced Traits
Pre-lecture Reflections
- How do traits in Rust compare to interfaces in Java or abstract base classes in Python?
- What are the benefits of default method implementations in traits?
- When would you use
impl Traitvs generic type parameters with trait bounds? - How do trait objects enable dynamic polymorphism in Rust?
Learning Objectives
By the end of this module, you will be able to:
- Define and implement traits for custom types
- Use trait bounds to constrain generic functions
- Understand different syntaxes for trait parameters (
impl Trait, generic bounds,whereclauses) - Return types that implement traits
Traits
From Traits: Defining Shared Behavior.
- 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.
- We can use trait bounds to specify that a generic type can be any type that has certain behavior.
Some other programming languages call this an interface.
Sample trait definition
The general idea is:
-
define method signatures as behaviors that need to be implemented by any type that implements the trait
-
We can also define default implementations of methods.
#![allow(unused)] fn main() { trait Person { // method header specifications // must be implemented by any type that implements the trait fn get_name(&self) -> String; fn get_age(&self) -> u32; // default implementation of a method fn description(&self) -> String { format!("{} ({})",self.get_name(),self.get_age()) } } }
Sample trait implementation 1
Let's look at a simple example of a trait implementation.
trait Person { // method header specifications // must be implemented by any type that implements the trait fn get_name(&self) -> String; fn get_age(&self) -> u32; // default implementation of a method fn description(&self) -> String { format!("{} ({})",self.get_name(),self.get_age()) } } #[derive(Debug)] struct SoccerPlayer { name: String, age: u32, team: String, } // Implement the `Person` trait for `SoccerPlayer` so that // it can be used as a `Person` object. impl Person for SoccerPlayer { fn get_age(&self) -> u32 { self.age } // We must implement all trait items fn get_name(&self) -> String { self.name.clone() } } // Implement a constructor for `SoccerPlayer` impl SoccerPlayer { fn create(name:String, age:u32, team:String) -> SoccerPlayer { SoccerPlayer{name,age,team} } } // Since `SoccerPlayer` implements the `Person` trait, // we can use the `description` method on instances of `SoccerPlayer`. fn main() { let zlatan = SoccerPlayer::create(String::from("Zlatan Ibrahimovic"), 40, String::from("AC Milan")); println!("{}", zlatan.description()); }
Sample trait implementation 2
Now let's look at another example of a trait implementation.
trait Person { // method header specifications // must be implemented by any type that implements the trait fn get_name(&self) -> String; fn get_age(&self) -> u32; // default implementation of a method fn description(&self) -> String { format!("{} ({})",self.get_name(),self.get_age()) } } #[derive(Debug)] struct RegularPerson { year_born: u32, first_name: String, middle_name: String, last_name: String, } impl Person for RegularPerson { fn get_age(&self) -> u32 { 2024 - self.year_born } fn get_name(&self) -> String { if self.middle_name == "" { format!("{} {}",self.first_name,self.last_name) } else { format!("{} {} {}",self.first_name,self.middle_name,self.last_name) } } } impl RegularPerson { fn create(first_name:String,middle_name:String,last_name:String,year_born:u32) -> RegularPerson { RegularPerson{first_name,middle_name,last_name,year_born} } } fn main() { let mlk = RegularPerson::create( String::from("Martin"), String::from("Luther"), String::from("King"), 1929 ); println!("{}", mlk.description()); }
Using traits in functions -- Trait Bounds
So now, we specify that we need a function that accepts an object that implements the Person trait.
#![allow(unused)] fn main() { // sample function accepting object implementing trait fn long_description(person: &impl Person) { println!("{}, who is {} years old", person.get_name(), person.get_age()); } }
This way we know we can call the get_name and get_age methods on the object that is passed to the function.
It allows us to specify a whole class of objects and know what methods are available on them.
Examples
We can see this in action with the two examples we saw earlier.
trait Person { // method header specifications // must be implemented by any type that implements the trait fn get_name(&self) -> String; fn get_age(&self) -> u32; // default implementation of a method fn description(&self) -> String { format!("{} ({})",self.get_name(),self.get_age()) } } #[derive(Debug)] struct SoccerPlayer { name: String, age: u32, team: String, } // Implement the `Person` trait for `SoccerPlayer` so that // it can be used as a `Person` object. impl Person for SoccerPlayer { fn get_age(&self) -> u32 { self.age } // We must implement all trait items fn get_name(&self) -> String { self.name.clone() } } // Implement a constructor for `SoccerPlayer` impl SoccerPlayer { fn create(name:String, age:u32, team:String) -> SoccerPlayer { SoccerPlayer{name,age,team} } } #[derive(Debug)] struct RegularPerson { year_born: u32, first_name: String, middle_name: String, last_name: String, } impl Person for RegularPerson { fn get_age(&self) -> u32 { 2024 - self.year_born } fn get_name(&self) -> String { if self.middle_name == "" { format!("{} {}",self.first_name,self.last_name) } else { format!("{} {} {}",self.first_name,self.middle_name,self.last_name) } } } impl RegularPerson { fn create(first_name:String,middle_name:String,last_name:String,year_born:u32) -> RegularPerson { RegularPerson{first_name,middle_name,last_name,year_born} } } // sample function accepting object implementing trait fn long_description(person: &impl Person) { println!("{}, who is {} years old", person.get_name(), person.get_age()); } fn main() { let mlk = RegularPerson::create( String::from("Martin"), String::from("Luther"), String::from("King"), 1929 ); let zlatan = SoccerPlayer::create(String::from("Zlatan Ibrahimovic"), 40, String::from("AC Milan")); long_description(&mlk); // we can pass a `RegularPerson` object to the function long_description(&zlatan); // we can pass a `SoccerPlayer` object to the function }
Using traits in functions: long vs. short form
There's a longer, generic version of the function that we can use.
trait Person { // method header specifications // must be implemented by any type that implements the trait fn get_name(&self) -> String; fn get_age(&self) -> u32; // default implementation of a method fn description(&self) -> String { format!("{} ({})",self.get_name(),self.get_age()) } } #[derive(Debug)] struct SoccerPlayer { name: String, age: u32, team: String, } // Implement the `Person` trait for `SoccerPlayer` so that // it can be used as a `Person` object. impl Person for SoccerPlayer { fn get_age(&self) -> u32 { self.age } // We must implement all trait items fn get_name(&self) -> String { self.name.clone() } } // Implement a constructor for `SoccerPlayer` impl SoccerPlayer { fn create(name:String, age:u32, team:String) -> SoccerPlayer { SoccerPlayer{name,age,team} } } #[derive(Debug)] struct RegularPerson { year_born: u32, first_name: String, middle_name: String, last_name: String, } impl Person for RegularPerson { fn get_age(&self) -> u32 { 2024 - self.year_born } fn get_name(&self) -> String { if self.middle_name == "" { format!("{} {}",self.first_name,self.last_name) } else { format!("{} {} {}",self.first_name,self.middle_name,self.last_name) } } } impl RegularPerson { fn create(first_name:String,middle_name:String,last_name:String,year_born:u32) -> RegularPerson { RegularPerson{first_name,middle_name,last_name,year_born} } } // short version fn long_description(person: &impl Person) { println!("{}, who is {} old", person.get_name(), person.get_age()); } // longer version fn long_description_2<T: Person>(person: &T) { println!("{}, who is {} old", person.get_name(), person.get_age()); } fn main() { let mlk = RegularPerson::create( String::from("Martin"), String::from("Luther"), String::from("King"), 1929 ); let zlatan = SoccerPlayer::create(String::from("Zlatan Ibrahimovic"), 40, String::from("AC Milan")); long_description(&zlatan); long_description_2(&zlatan); long_description(&mlk); long_description_2(&mlk); }
So what's up with the different ways to specify traits (It's complicated!!!!)
Optional: You can skip this if you want.
&impl and &T-> static dispatch (also relevant in the context of return values)&Trestricts the type especially if you plan to pass multiple arguments of the same type (relevant to inputs)- Read https://joshleeb.com/posts/rust-traits-and-trait-objects if you want to dig deep but without a background in programming languages and compilers this will not be possible to understand.
Using traits in functions: multiple traits
trait Person { // method header specifications // must be implemented by any type that implements the trait fn get_name(&self) -> String; fn get_age(&self) -> u32; // default implementation of a method fn description(&self) -> String { format!("{} ({})",self.get_name(),self.get_age()) } } #[derive(Debug)] struct SoccerPlayer { name: String, age: u32, team: String, } // Implement the `Person` trait for `SoccerPlayer` so that // it can be used as a `Person` object. impl Person for SoccerPlayer { fn get_age(&self) -> u32 { self.age } // We must implement all trait items fn get_name(&self) -> String { self.name.clone() } } // Implement a constructor for `SoccerPlayer` impl SoccerPlayer { fn create(name:String, age:u32, team:String) -> SoccerPlayer { SoccerPlayer{name,age,team} } } #[derive(Debug)] struct RegularPerson { year_born: u32, first_name: String, middle_name: String, last_name: String, } impl Person for RegularPerson { fn get_age(&self) -> u32 { 2024 - self.year_born } fn get_name(&self) -> String { if self.middle_name == "" { format!("{} {}",self.first_name,self.last_name) } else { format!("{} {} {}",self.first_name,self.middle_name,self.last_name) } } } impl RegularPerson { fn create(first_name:String,middle_name:String,last_name:String,year_born:u32) -> RegularPerson { RegularPerson{first_name,middle_name,last_name,year_born} } } // sample function accepting object implementing trait fn long_description(person: &impl Person) { println!("{}, who is {} years old", person.get_name(), person.get_age()); } use std::fmt::Debug; fn multiple_1(person: &(impl Person + Debug)) { println!("{:?}",person); println!("Age: {}",person.get_age()); } fn main() { let mlk = RegularPerson::create( String::from("Martin"), String::from("Luther"), String::from("King"), 1929 ); let zlatan = SoccerPlayer::create(String::from("Zlatan Ibrahimovic"), 40, String::from("AC Milan")); multiple_1(&zlatan); multiple_1(&mlk); }
Using traits in functions: multiple traits
trait Person { // method header specifications // must be implemented by any type that implements the trait fn get_name(&self) -> String; fn get_age(&self) -> u32; // default implementation of a method fn description(&self) -> String { format!("{} ({})",self.get_name(),self.get_age()) } } #[derive(Debug)] struct SoccerPlayer { name: String, age: u32, team: String, } // Implement the `Person` trait for `SoccerPlayer` so that // it can be used as a `Person` object. impl Person for SoccerPlayer { fn get_age(&self) -> u32 { self.age } // We must implement all trait items fn get_name(&self) -> String { self.name.clone() } } // Implement a constructor for `SoccerPlayer` impl SoccerPlayer { fn create(name:String, age:u32, team:String) -> SoccerPlayer { SoccerPlayer{name,age,team} } } #[derive(Debug)] struct RegularPerson { year_born: u32, first_name: String, middle_name: String, last_name: String, } impl Person for RegularPerson { fn get_age(&self) -> u32 { 2024 - self.year_born } fn get_name(&self) -> String { if self.middle_name == "" { format!("{} {}",self.first_name,self.last_name) } else { format!("{} {} {}",self.first_name,self.middle_name,self.last_name) } } } impl RegularPerson { fn create(first_name:String,middle_name:String,last_name:String,year_born:u32) -> RegularPerson { RegularPerson{first_name,middle_name,last_name,year_born} } } // sample function accepting object implementing trait fn long_description(person: &impl Person) { println!("{}, who is {} years old", person.get_name(), person.get_age()); } use std::fmt::Debug; // three options, useful for different settings // This is good if you want to pass many parameters to the function // and the parameters are of different types fn multiple_1(person: &(impl Person + Debug)) { println!("{:?}",person); println!("Age: {}",person.get_age()); } // This is better if you want all your parameters to be of the same type fn multiple_2<T: Person + Debug>(person: &T) { println!("{:?}",person); println!("Age: {}",person.get_age()); } // This is like option 2 but easier to read if your parameter // combines many traits fn multiple_3<T>(person: &T) where T: Person + Debug { println!("{:?}",person); println!("Age: {}",person.get_age()); } fn main() { let mlk = RegularPerson::create( String::from("Martin"), String::from("Luther"), String::from("King"), 1929 ); multiple_1(&mlk); multiple_2(&mlk); multiple_3(&mlk); }
Returning types implementing a trait
trait Person { // method header specifications // must be implemented by any type that implements the trait fn get_name(&self) -> String; fn get_age(&self) -> u32; // default implementation of a method fn description(&self) -> String { format!("{} ({})",self.get_name(),self.get_age()) } } #[derive(Debug)] struct SoccerPlayer { name: String, age: u32, team: String, } // Implement the `Person` trait for `SoccerPlayer` so that // it can be used as a `Person` object. impl Person for SoccerPlayer { fn get_age(&self) -> u32 { self.age } // We must implement all trait items fn get_name(&self) -> String { self.name.clone() } } // Implement a constructor for `SoccerPlayer` impl SoccerPlayer { fn create(name:String, age:u32, team:String) -> SoccerPlayer { SoccerPlayer{name,age,team} } } #[derive(Debug)] struct RegularPerson { year_born: u32, first_name: String, middle_name: String, last_name: String, } impl Person for RegularPerson { fn get_age(&self) -> u32 { 2024 - self.year_born } fn get_name(&self) -> String { if self.middle_name == "" { format!("{} {}",self.first_name,self.last_name) } else { format!("{} {} {}",self.first_name,self.middle_name,self.last_name) } } } impl RegularPerson { fn create(first_name:String,middle_name:String,last_name:String,year_born:u32) -> RegularPerson { RegularPerson{first_name,middle_name,last_name,year_born} } } // sample function accepting object implementing trait fn long_description(person: &impl Person) { println!("{}, who is {} years old", person.get_name(), person.get_age()); } fn get_zlatan() -> impl Person { SoccerPlayer::create(String::from("Zlatan Ibrahimovic"), 40, String::from("AC Milan")) } fn main() { let zlatan = SoccerPlayer::create(String::from("Zlatan Ibrahimovic"), 40, String::from("AC Milan")); let zlatan_2 = get_zlatan(); long_description(&zlatan_2); }
Recap
- Traits are a way to define shared behavior that can be implemented by different 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.
In-Class Activity: Practicing Traits and Trait Bounds
Time: 10 minutes
Instructions
Work individually or in pairs. Complete as many exercises as you can in 10 minutes. You can test your code in the Rust playground or in your local environment.
Exercise 1: Define and Implement a Trait (3 minutes)
Define a trait called Describable with a method describe() that returns a String. Then implement it for the Book struct.
// TODO: Define the Describable trait trait Describable { // Your code here } struct Book { title: String, author: String, pages: u32, } // TODO: Implement Describable for Book // The describe() method should return a string like: // "'The Rust Book' by Steve Klabnik (500 pages)" fn main() { let book = Book { title: String::from("The Rust Book"), author: String::from("Steve Klabnik"), pages: 500, }; println!("{}", book.describe()); }
Hint
Remember the trait definition syntax:
#![allow(unused)] fn main() { trait TraitName { fn method_name(&self) -> ReturnType; } }
And implementation:
#![allow(unused)] fn main() { impl TraitName for StructName { fn method_name(&self) -> ReturnType { // implementation } } }
Exercise 2: Multiple Trait Bounds with Where Clause (3 minutes)
Refactor the following function to use a where clause instead of inline trait bounds. Then add a call to the function in main.
use std::fmt::{Debug, Display}; // TODO: Refactor this to use a where clause fn print_info<T: Debug + Display + PartialOrd>(item: &T, compare_to: &T) { println!("Item: {}", item); println!("Debug: {:?}", item); if item > compare_to { println!("Item is greater than comparison value"); } } fn main() { // TODO: Call print_info with appropriate arguments }
Hint
The where clause syntax is:
#![allow(unused)] fn main() { fn function_name<T>(params) -> ReturnType where T: Trait1 + Trait2 { // body } }
Bonus Challenge (if you finish early)
Create a trait called Area with a method area() that returns f64. Implement it for both Circle and Rectangle structs. Then write a generic function print_area that accepts anything implementing the Area trait.
// TODO: Define the Area trait // TODO: Define Circle struct (radius: f64) // TODO: Define Rectangle struct (width: f64, height: f64) // TODO: Implement Area for Circle (π * r²) // TODO: Implement Area for Rectangle (width * height) // TODO: Write a generic function that prints the area // fn print_area(...) { ... } fn main() { let circle = Circle { radius: 5.0 }; let rectangle = Rectangle { width: 4.0, height: 6.0 }; print_area(&circle); print_area(&rectangle); }
Solutions
Click to reveal solutions (try on your own first!)
Exercise 1 Solution
trait Describable { fn describe(&self) -> String; } struct Book { title: String, author: String, pages: u32, } impl Describable for Book { fn describe(&self) -> String { format!("'{}' by {} ({} pages)", self.title, self.author, self.pages) } } fn main() { let book = Book { title: String::from("The Rust Book"), author: String::from("Steve Klabnik"), pages: 500, }; println!("{}", book.describe()); }
Exercise 2 Solution
use std::fmt::{Debug, Display}; fn print_info<T>(item: &T, compare_to: &T) where T: Debug + Display + PartialOrd { println!("Item: {}", item); println!("Debug: {:?}", item); if item > compare_to { println!("Item is greater than comparison value"); } } fn main() { print_info(&42, &10); print_info(&3.14, &2.71); print_info(&'z', &'a'); }
Bonus Solution
trait Area { fn area(&self) -> f64; } struct Circle { radius: f64, } struct Rectangle { width: f64, height: f64, } impl Area for Circle { fn area(&self) -> f64 { std::f64::consts::PI * self.radius * self.radius } } impl Area for Rectangle { fn area(&self) -> f64 { self.width * self.height } } fn print_area(shape: &impl Area) { println!("Area: {:.2}", shape.area()); } // Alternative using generic syntax: // fn print_area<T: Area>(shape: &T) { // println!("Area: {:.2}", shape.area()); // } fn main() { let circle = Circle { radius: 5.0 }; let rectangle = Rectangle { width: 4.0, height: 6.0 }; print_area(&circle); print_area(&rectangle); }