Defining and Implementing Traits
Traits define shared behavior — similar to interfaces in other languages. They specify method signatures that types must implement, enabling polymorphism and code reuse without inheritance.
// Define a trait
trait Summary {
// Required method — implementors must define this
fn summarize(&self) -> String;
// Default implementation — can be overridden
fn preview(&self) -> String {
format!("{}...", &self.summarize()[..20.min(self.summarize().len())])
}
}
struct Article {
title: String,
author: String,
content: String,
}
impl Summary for Article {
fn summarize(&self) -> String {
format!("{}, by {} — {}", self.title, self.author, &self.content[..50.min(self.content.len())])
}
// preview() uses the default implementation
}
struct Tweet {
username: String,
content: String,
}
impl Summary for Tweet {
fn summarize(&self) -> String {
format!("@{}: {}", self.username, self.content)
}
fn preview(&self) -> String {
format!("@{} tweeted", self.username) // Override default
}
}
fn main() {
let article = Article {
title: String::from("Rust 2024"),
author: String::from("Ferris"),
content: String::from("Rust continues to grow with amazing new features and improvements"),
};
let tweet = Tweet {
username: String::from("rustlang"),
content: String::from("Rust 1.80 is out!"),
};
println!("{}", article.summarize());
println!("{}", tweet.summarize());
println!("{}", tweet.preview());
}
Generics
// Generic function
fn largest(list: &[T]) -> &T {
let mut largest = &list[0];
for item in &list[1..] {
if item > largest {
largest = item;
}
}
largest
}
// Generic struct
struct Point {
x: T,
y: T,
}
// Different types for x and y
struct MixedPoint {
x: T,
y: U,
}
// Implement methods on generic struct
impl Point {
fn display(&self) {
println!("({}, {})", self.x, self.y);
}
}
// Implement methods only for specific types
impl Point {
fn distance_from_origin(&self) -> f64 {
(self.x.powi(2) + self.y.powi(2)).sqrt()
}
}
fn main() {
let numbers = vec![34, 50, 25, 100, 65];
println!("Largest: {}", largest(&numbers));
let chars = vec!['a', 'z', 'm', 'b'];
println!("Largest: {}", largest(&chars));
let int_point = Point { x: 5, y: 10 };
let float_point = Point { x: 1.5, y: 3.7 };
int_point.display();
float_point.display();
println!("Distance: {:.2}", float_point.distance_from_origin());
let mixed = MixedPoint { x: 5, y: 3.14 };
println!("({}, {})", mixed.x, mixed.y);
}
Trait Bounds and Where Clauses
use std::fmt::{Display, Debug};
// Trait bound syntax
fn print_summary(item: &T) {
println!("{}", item.summarize());
}
// impl Trait syntax (sugar for simple cases)
fn print_summary_alt(item: &impl Summary) {
println!("{}", item.summarize());
}
// Multiple bounds with +
fn display_and_debug(item: &T) {
println!("Display: {item}");
println!("Debug: {item:?}");
}
// Where clauses for complex bounds
fn complex_function(t: &T, u: &U) -> String
where
T: Display + Clone,
U: Debug + Summary,
{
format!("{t} and {:?}", u)
}
// Returning types that implement traits
fn make_summary() -> impl Summary {
Tweet {
username: String::from("bot"),
content: String::from("Hello from a function!"),
}
}
trait Summary { fn summarize(&self) -> String; }
struct Tweet { username: String, content: String }
impl Summary for Tweet {
fn summarize(&self) -> String { format!("@{}: {}", self.username, self.content) }
}
Associated Types and Supertraits
// Associated types — specify a type as part of the trait
trait Iterator {
type Item; // Associated type
fn next(&mut self) -> Option;
}
struct Counter {
count: u32,
max: u32,
}
impl Iterator for Counter {
type Item = u32; // Concrete type for this implementation
fn next(&mut self) -> Option {
if self.count < self.max {
self.count += 1;
Some(self.count)
} else {
None
}
}
}
// Supertraits: require another trait
trait PrettyPrint: std::fmt::Display {
fn pretty_print(&self) {
println!("=== {} ===", self); // Can use Display methods!
}
}
// Implement for anything that's Display
impl PrettyPrint for T {}
fn main() {
let mut counter = Counter { count: 0, max: 5 };
while let Some(n) = counter.next() {
print!("{n} ");
}
println!();
42.pretty_print();
"hello".pretty_print();
}
Trait Objects (Dynamic Dispatch)
trait Drawable {
fn draw(&self);
fn area(&self) -> f64;
}
struct Circle { radius: f64 }
struct Square { side: f64 }
impl Drawable for Circle {
fn draw(&self) { println!("Drawing circle (r={})", self.radius); }
fn area(&self) -> f64 { std::f64::consts::PI * self.radius * self.radius }
}
impl Drawable for Square {
fn draw(&self) { println!("Drawing square (s={})", self.side); }
fn area(&self) -> f64 { self.side * self.side }
}
// dyn Trait = dynamic dispatch (runtime polymorphism)
fn draw_all(shapes: &[Box]) {
for shape in shapes {
shape.draw();
println!(" Area: {:.2}", shape.area());
}
}
// &dyn Trait also works (no heap allocation)
fn print_area(shape: &dyn Drawable) {
println!("Area: {:.2}", shape.area());
}
fn main() {
let shapes: Vec> = vec![
Box::new(Circle { radius: 5.0 }),
Box::new(Square { side: 3.0 }),
Box::new(Circle { radius: 2.5 }),
];
draw_all(&shapes);
let circle = Circle { radius: 10.0 };
print_area(&circle);
}
Key Takeaways
- ✅ Traits define shared behavior — Rust's alternative to interfaces and inheritance
- ✅ Generics with trait bounds enable type-safe polymorphism at compile time
- ✅ Where clauses keep complex trait bounds readable
- ✅ Associated types simplify traits that have one logical output type
- ✅ Use
dyn Traitfor runtime polymorphism when you need heterogeneous collections