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.