#14 Traits
trait, impl, dyn
What is a trait?
A trait defines shared behavior. It is similar to interfaces in other languages: it declares a set of methods that types can implement. Traits enable polymorphism in Rust.
The standard library is full of traits: Display, Debug, Clone, Iterator, and many more.
Defining traits
You define a trait with the trait keyword and list the method signatures:
trait Animal {
fn name(&self) -> &str;
fn sound(&self) -> &str;
}A trait can require multiple methods. Each method signature ends with a semicolon -- no body is provided here, just the contract.
Implementing traits
You implement a trait for a type with impl TraitName for Type:
struct Dog {
name: String,
}
struct Cat {
name: String,
}
impl Animal for Dog {
fn name(&self) -> &str {
&self.name
}
fn sound(&self) -> &str {
"Woof!"
}
}
impl Animal for Cat {
fn name(&self) -> &str {
&self.name
}
fn sound(&self) -> &str {
"Meow!"
}
}
fn main() {
let dog = Dog { name: String::from("Rex") };
let cat = Cat { name: String::from("Whiskers") };
println!("{} says: {}", dog.name(), dog.sound());
println!("{} says: {}", cat.name(), cat.sound());
}Default methods
Traits can provide default implementations. Types can override them or use the defaults:
trait Describable {
fn name(&self) -> &str;
// Default implementation
fn description(&self) -> String {
format!("An object called {}", self.name())
}
}
struct Dog {
name: String,
}
impl Describable for Dog {
fn name(&self) -> &str {
&self.name
}
// Override the default
fn description(&self) -> String {
format!("{}: A loyal companion", self.name())
}
}
struct Cat {
name: String,
}
impl Describable for Cat {
fn name(&self) -> &str {
&self.name
}
fn description(&self) -> String {
format!("{}: An independent feline", self.name())
}
}
fn main() {
let dog = Dog { name: String::from("Dog") };
let cat = Cat { name: String::from("Cat") };
println!("{}", dog.description());
println!("{}", cat.description());
}Trait bounds
You can use traits as constraints on generic functions. This is called a trait bound -- it ensures the type has the required behavior:
trait Shape {
fn area(&self) -> f64;
}
struct Circle {
radius: f64,
}
struct Square {
side: f64,
}
impl Shape for Circle {
fn area(&self) -> f64 {
std::f64::consts::PI * self.radius * self.radius
}
}
impl Shape for Square {
fn area(&self) -> f64 {
self.side * self.side
}
}
// Trait bound: T must implement Shape
fn print_area<T: Shape>(shape: &T) {
println!("Area: {:.2}", shape.area());
}
// Alternative syntax with impl Trait
fn print_area_alt(shape: &impl Shape) {
println!("Area: {:.2}", shape.area());
}
fn main() {
let circle = Circle { radius: 5.0 };
let square = Square { side: 5.0 };
print_area(&circle); // Area: 78.54
print_area(&square); // Area: 25.00
}You can also use dyn Trait for dynamic dispatch with trait objects:
fn describe(shape: &dyn Shape) {
println!("This shape has area: {:.2}", shape.area());
}
fn main() {
let shapes: Vec<Box<dyn Shape>> = vec![
Box::new(Circle { radius: 3.0 }),
Box::new(Square { side: 4.0 }),
];
for shape in &shapes {
describe(shape.as_ref());
}
}Summary
trait Name { // Define a trait
fn method(&self) -> Type; // Required method
fn default(&self) { ... } // Default method
}
impl Name for MyStruct { ... } // Implement trait
fn foo<T: Trait>(x: &T) // Trait bound
fn foo(x: &impl Trait) // Shorthand
fn foo(x: &dyn Trait) // Dynamic dispatch
Box<dyn Trait> // Trait object on heapYour turn
Try running the traits demo with cargo run and check for errors with cargo check: