Traits
Traits define shared behavior that types can implement. They're similar to interfaces in other languages but more powerful, enabling zero-cost abstractions and flexible code organization. Traits are fundamental to Rust's type system and enable many advanced patterns.
Basic Trait Definition
A trait defines a set of methods that types can implement:
trait Drawable { fn draw(&self); fn area(&self) -> f64; } struct Circle { radius: f64, } struct Rectangle { width: f64, height: f64, } impl Drawable for Circle { fn draw(&self) { println!("Drawing a circle with radius {}", self.radius); } fn area(&self) -> f64 { std::f64::consts::PI * self.radius * self.radius } } impl Drawable for Rectangle { fn draw(&self) { println!("Drawing a rectangle {}x{}", self.width, self.height); } fn area(&self) -> f64 { self.width * self.height } } fn main() { let circle = Circle { radius: 5.0 }; let rectangle = Rectangle { width: 3.0, height: 4.0 }; circle.draw(); println!("Circle area: {}", circle.area()); rectangle.draw(); println!("Rectangle area: {}", rectangle.area()); } Default Implementations
Traits can provide default implementations for methods:
trait Greet { fn name(&self) -> &str; // Default implementation fn greet(&self) { println!("Hello, my name is {}", self.name()); } // Another default implementation fn formal_greet(&self) { println!("Good day. I am {}", self.name()); } } struct Person { name: String, } struct Robot { model: String, } impl Greet for Person { fn name(&self) -> &str { &self.name } // Override default implementation fn greet(&self) { println!("Hi there! I'm {}", self.name()); } } impl Greet for Robot { fn name(&self) -> &str { &self.model } // Use default implementations for greet() and formal_greet() } fn main() { let person = Person { name: "Alice".to_string() }; let robot = Robot { model: "R2D2".to_string() }; person.greet(); // Uses overridden implementation person.formal_greet(); // Uses default implementation robot.greet(); // Uses default implementation robot.formal_greet(); // Uses default implementation } Trait Bounds
Use trait bounds to specify that generic types must implement certain traits:
trait Printable { fn print(&self); } struct Document { content: String, } struct Image { filename: String, } impl Printable for Document { fn print(&self) { println!("Printing document: {}", self.content); } } impl Printable for Image { fn print(&self) { println!("Printing image: {}", self.filename); } } // Function with trait bound fn print_item<T: Printable>(item: &T) { item.print(); } // Alternative syntax fn print_item_alt<T>(item: &T) where T: Printable, { item.print(); } // Multiple trait bounds fn process_item<T>(item: &T) where T: Printable + std::fmt::Debug, { println!("Processing: {:?}", item); item.print(); } fn main() { let doc = Document { content: "Hello, world!".to_string() }; let img = Image { filename: "photo.jpg".to_string() }; print_item(&doc); print_item(&img); } Associated Types
Associated types allow traits to define placeholder types:
trait Iterator { type Item; // Associated type fn next(&mut self) -> Option<Self::Item>; } struct Counter { current: usize, max: usize, } impl Counter { fn new(max: usize) -> Counter { Counter { current: 0, max } } } impl Iterator for Counter { type Item = usize; // Specify the associated type fn next(&mut self) -> Option<Self::Item> { if self.current < self.max { let current = self.current; self.current += 1; Some(current) } else { None } } } // Generic function using associated type fn collect_items<I: Iterator>(mut iter: I) -> Vec<I::Item> { let mut items = Vec::new(); while let Some(item) = iter.next() { items.push(item); } items } fn main() { let mut counter = Counter::new(5); let items = collect_items(counter); println!("Items: {:?}", items); } Trait Objects
Use trait objects for dynamic dispatch:
trait Animal { fn make_sound(&self); fn name(&self) -> &str; } struct Dog { name: String, } struct Cat { name: String, } impl Animal for Dog { fn make_sound(&self) { println!("Woof!"); } fn name(&self) -> &str { &self.name } } impl Animal for Cat { fn make_sound(&self) { println!("Meow!"); } fn name(&self) -> &str { &self.name } } // Function accepting trait objects fn animal_sounds(animals: &[Box<dyn Animal>]) { for animal in animals { println!("{} says:", animal.name()); animal.make_sound(); } } fn main() { let animals: Vec<Box<dyn Animal>> = vec![ Box::new(Dog { name: "Buddy".to_string() }), Box::new(Cat { name: "Whiskers".to_string() }), Box::new(Dog { name: "Max".to_string() }), ]; animal_sounds(&animals); } Derived Traits
Rust can automatically implement common traits:
#[derive(Debug, Clone, PartialEq, Eq)] struct Point { x: i32, y: i32, } #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] struct Person { name: String, age: u32, } fn main() { let p1 = Point { x: 1, y: 2 }; let p2 = Point { x: 1, y: 2 }; let p3 = p1.clone(); // Debug println!("Point: {:?}", p1); // PartialEq println!("p1 == p2: {}", p1 == p2); println!("p1 == p3: {}", p1 == p3); let alice = Person { name: "Alice".to_string(), age: 30 }; let bob = Person { name: "Bob".to_string(), age: 25 }; // Ordering (lexicographic by fields) println!("alice < bob: {}", alice < bob); } Standard Library Traits
Display and Debug
use std::fmt; struct Temperature { celsius: f64, } impl fmt::Display for Temperature { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}°C", self.celsius) } } impl fmt::Debug for Temperature { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "Temperature {{ celsius: {} }}", self.celsius) } } fn main() { let temp = Temperature { celsius: 25.0 }; println!("Display: {}", temp); // Uses Display trait println!("Debug: {:?}", temp); // Uses Debug trait println!("Pretty Debug: {:#?}", temp); } From and Into
struct Celsius(f64); struct Fahrenheit(f64); impl From<Fahrenheit> for Celsius { fn from(f: Fahrenheit) -> Self { Celsius((f.0 - 32.0) * 5.0 / 9.0) } } // Into is automatically implemented when From is implemented impl From<Celsius> for Fahrenheit { fn from(c: Celsius) -> Self { Fahrenheit(c.0 * 9.0 / 5.0 + 32.0) } } fn main() { let freezing_f = Fahrenheit(32.0); let freezing_c: Celsius = freezing_f.into(); println!("32°F = {}°C", freezing_c.0); let boiling_c = Celsius(100.0); let boiling_f = Fahrenheit::from(boiling_c); println!("100°C = {}°F", boiling_f.0); } TryFrom and TryInto
use std::convert::TryFrom; use std::error::Error; use std::fmt; #[derive(Debug)] struct PositiveInteger(u32); #[derive(Debug)] struct PositiveIntegerError; impl fmt::Display for PositiveIntegerError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "Value must be positive") } } impl Error for PositiveIntegerError {} impl TryFrom<i32> for PositiveInteger { type Error = PositiveIntegerError; fn try_from(value: i32) -> Result<Self, Self::Error> { if value > 0 { Ok(PositiveInteger(value as u32)) } else { Err(PositiveIntegerError) } } } fn main() { // Successful conversion match PositiveInteger::try_from(42) { Ok(pos_int) => println!("Created: {:?}", pos_int), Err(e) => println!("Error: {}", e), } // Failed conversion match PositiveInteger::try_from(-5) { Ok(pos_int) => println!("Created: {:?}", pos_int), Err(e) => println!("Error: {}", e), } } Advanced Trait Patterns
Supertraits
trait Animal { fn name(&self) -> &str; } // Mammal is a supertrait of Animal trait Mammal: Animal { fn fur_color(&self) -> &str; } struct Dog { name: String, fur_color: String, } impl Animal for Dog { fn name(&self) -> &str { &self.name } } impl Mammal for Dog { fn fur_color(&self) -> &str { &self.fur_color } } fn describe_mammal<T: Mammal>(mammal: &T) { println!("{} has {} fur", mammal.name(), mammal.fur_color()); } fn main() { let dog = Dog { name: "Buddy".to_string(), fur_color: "brown".to_string(), }; describe_mammal(&dog); } Associated Constants
trait MathConstants { const PI: f64; const E: f64; } struct Calculator; impl MathConstants for Calculator { const PI: f64 = 3.14159265359; const E: f64 = 2.71828182846; } trait Shape { const SIDES: u32; fn area(&self) -> f64; } struct Triangle { base: f64, height: f64, } impl Shape for Triangle { const SIDES: u32 = 3; fn area(&self) -> f64 { 0.5 * self.base * self.height } } fn main() { println!("PI = {}", Calculator::PI); println!("E = {}", Calculator::E); let triangle = Triangle { base: 10.0, height: 5.0 }; println!("Triangle has {} sides", Triangle::SIDES); println!("Triangle area: {}", triangle.area()); } Generic Traits
trait Add<RHS = Self> { type Output; fn add(self, rhs: RHS) -> Self::Output; } #[derive(Debug, PartialEq)] struct Point { x: i32, y: i32, } impl Add for Point { type Output = Point; fn add(self, other: Point) -> Point { Point { x: self.x + other.x, y: self.y + other.y, } } } // Different implementation for adding integers to points impl Add<i32> for Point { type Output = Point; fn add(self, scalar: i32) -> Point { Point { x: self.x + scalar, y: self.y + scalar, } } } fn main() { let p1 = Point { x: 1, y: 2 }; let p2 = Point { x: 3, y: 4 }; let p3 = p1.add(p2); println!("Point + Point: {:?}", p3); let p4 = Point { x: 1, y: 2 }; let p5 = p4.add(5); println!("Point + i32: {:?}", p5); } Conditional Trait Implementation
trait Summary { fn summarize(&self) -> String; } // Implement Summary for any type that implements Display impl<T: std::fmt::Display> Summary for T { fn summarize(&self) -> String { format!("Summary: {}", self) } } fn main() { let number = 42; let text = "Hello, world!"; // These work because i32 and &str implement Display println!("{}", number.summarize()); println!("{}", text.summarize()); } Marker Traits
// Marker trait - no methods trait Serializable {} struct User { name: String, email: String, } struct Password { hash: String, } // Only User implements Serializable impl Serializable for User {} fn serialize<T: Serializable>(item: &T) -> String { // In real implementation, would serialize the item "serialized data".to_string() } fn main() { let user = User { name: "Alice".to_string(), email: "[email protected]".to_string(), }; let _serialized = serialize(&user); // This works let password = Password { hash: "secret_hash".to_string(), }; // This would not compile: // let serialized = serialize(&password); } Trait Bounds in Practice
Where Clauses
use std::fmt::Debug; use std::cmp::PartialOrd; // Complex trait bounds are clearer with where clauses fn compare_and_display<T, U>(t: &T, u: &U) -> std::cmp::Ordering where T: Debug + PartialOrd<U>, U: Debug, { println!("Comparing {:?} with {:?}", t, u); t.partial_cmp(u).unwrap() } fn main() { let result = compare_and_display(&5, &3); println!("Result: {:?}", result); } Higher-Ranked Trait Bounds
fn call_with_ref<F>(f: F) where F: for<'a> Fn(&'a str) -> &'a str, { let s = "hello"; let result = f(s); println!("Result: {}", result); } fn identity(s: &str) -> &str { s } fn main() { call_with_ref(identity); call_with_ref(|s| s); } Best Practices
1. Use Descriptive Trait Names
// Good: Clear purpose trait Drawable { fn draw(&self); } trait Comparable { fn compare(&self, other: &Self) -> std::cmp::Ordering; } // Avoid: Vague names trait Process { fn do_thing(&self); } 2. Keep Traits Focused
// Good: Single responsibility trait Readable { fn read(&self) -> String; } trait Writable { fn write(&mut self, data: &str); } // Avoid: Too many responsibilities trait FileHandler { fn read(&self) -> String; fn write(&mut self, data: &str); fn compress(&self) -> Vec<u8>; fn encrypt(&self, key: &str) -> Vec<u8>; fn backup(&self, location: &str) -> bool; } 3. Use Associated Types for Strong Coupling
// Good: Associated type when there's one logical choice trait Iterator { type Item; fn next(&mut self) -> Option<Self::Item>; } // Use generics when there could be multiple implementations trait Add<RHS = Self> { type Output; fn add(self, rhs: RHS) -> Self::Output; } 4. Provide Default Implementations When Sensible
trait Logger { fn log(&self, message: &str); // Default implementations for convenience fn info(&self, message: &str) { self.log(&format!("INFO: {}", message)); } fn error(&self, message: &str) { self.log(&format!("ERROR: {}", message)); } } 5. Use Trait Objects Judiciously
// Good: When you need runtime polymorphism fn process_shapes(shapes: &[Box<dyn Drawable>]) { for shape in shapes { shape.draw(); } } // Better: When you can use generics fn process_shapes_generic<T: Drawable>(shapes: &[T]) { for shape in shapes { shape.draw(); } } Traits are central to Rust's type system and enable powerful abstractions while maintaining zero-cost performance. They provide a way to define shared behavior, implement polymorphism, and create flexible APIs that work with many different types. Understanding traits deeply is essential for writing idiomatic and effective Rust code.