Traits - Defining Shared Behavior
Traits are Rust's way of defining shared behavior. They're similar to interfaces in other languages, allowing you to define a set of methods that types must implement. Traits enable polymorphism and code reuse in a type-safe way.
What Are Traits?
A trait defines functionality a particular type has and can share with other types. We use traits to define shared behavior in an abstract way.
trait Summary {
fn summarize(&self) -> String;
}
Defining and Implementing Traits
Click Run to execute your code
Default Implementations
Traits can provide default method implementations that types can use or override:
Click Run to execute your code
Traits as Parameters
You can use traits to accept multiple types that implement the same trait:
Click Run to execute your code
Two Syntaxes for Trait Parameters
| Syntax | Example | Use Case |
|---|---|---|
impl Trait |
fn notify(item: &impl Summary) |
Simple cases, more concise |
| Trait Bound | fn notify<T: Summary>(item: &T) |
Complex bounds, multiple parameters |
Multiple Trait Bounds
You can require a type to implement multiple traits:
// Using + to specify multiple traits
fn notify(item: &(impl Summary + Display)) {
// item must implement both Summary and Display
}
// Generic syntax
fn notify(item: &T) {
// Same thing
}
// where clause for readability
fn some_function(t: &T, u: &U) -> i32
where
T: Display + Clone,
U: Clone + Debug,
{
// Function body
}
Returning Types that Implement Traits
Click Run to execute your code
impl Trait as a return type,
you can only return ONE concrete type. You can't return different types even if
they both implement the trait.
Common Standard Library Traits
| Trait | Purpose | Example |
|---|---|---|
Debug |
Formatting with {:?} | #[derive(Debug)] |
Clone |
Explicit duplication | let y = x.clone(); |
Copy |
Implicit duplication | let y = x; |
Display |
User-facing output | println!("{}", x) |
PartialEq |
Equality comparison | x == y |
PartialOrd |
Ordering comparison | x < y |
Deriving Traits
Many common traits can be automatically implemented using #[derive]:
#[derive(Debug, Clone, PartialEq, PartialOrd)]
struct Point {
x: i32,
y: i32,
}
let p1 = Point { x: 5, y: 10 };
let p2 = p1.clone();
println!("{:?}", p1); // Uses Debug
println!("{}", p1 == p2); // Uses PartialEq
Best Practices
- Use traits for shared behavior: Define common functionality across types
- Provide default implementations: Make traits easier to implement
- Derive when possible: Use
#[derive]for standard traits - Use impl Trait for simple cases: More concise than generics
- Use where clauses: For complex trait bounds
- Keep traits focused: Single responsibility principle
Common Mistakes
1. Trying to return different types with impl Trait
// Wrong - Can't return different types
fn get_summary(switch: bool) -> impl Summary {
if switch {
NewsArticle { ... } // Type 1
} else {
Tweet { ... } // Type 2 - Error!
}
}
// Correct - Use Box for dynamic dispatch
fn get_summary(switch: bool) -> Box {
if switch {
Box::new(NewsArticle { ... })
} else {
Box::new(Tweet { ... })
}
}
2. Forgetting to implement required methods
// Wrong - Missing required method
trait Summary {
fn summarize(&self) -> String;
fn author(&self) -> String;
}
impl Summary for Article {
fn summarize(&self) -> String {
String::from("Article")
}
// Error: missing author() implementation
}
// Correct - Implement all required methods
impl Summary for Article {
fn summarize(&self) -> String {
String::from("Article")
}
fn author(&self) -> String {
String::from("Unknown")
}
}
3. Orphan rule violation
// Wrong - Can't implement external trait for external type
impl Display for Vec { // Error: orphan rule
// Can't implement Display (std) for Vec (std)
}
// Correct - Wrap in newtype pattern
struct MyVec(Vec);
impl Display for MyVec { // OK!
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(f, "{:?}", self.0)
}
}
Exercise: Traits Practice
Task: Implement Drawable and Area traits for shapes.
Click Run to execute your code
Show Solution
trait Drawable {
fn draw(&self) -> String;
}
trait Area {
fn area(&self) -> f64;
}
impl Drawable for Circle {
fn draw(&self) -> String {
format!("Circle with radius {}", self.radius)
}
}
impl Area for Circle {
fn area(&self) -> f64 {
std::f64::consts::PI * self.radius * self.radius
}
}
impl Drawable for Rectangle {
fn draw(&self) -> String {
format!("Rectangle {}x{}", self.width, self.height)
}
}
impl Area for Rectangle {
fn area(&self) -> f64 {
self.width * self.height
}
}
fn print_drawing(item: &impl Drawable) {
println!("{}", item.draw());
}
fn total_area(shapes: &[&T]) -> f64 {
shapes.iter().map(|s| s.area()).sum()
}
Summary
- Traits define shared behavior across types
- Similar to interfaces in other languages
- Can have default implementations
- Use impl Trait or generics for parameters
- Trait bounds constrain generic types
- Multiple bounds with
+operator - where clauses for complex bounds
- Derive common traits automatically
- Enable polymorphism in Rust
- Orphan rule: trait or type must be local
What's Next?
Now that you understand traits, you're ready to learn about Generics - how to write code that works with any type. Generics and traits work together to enable powerful, reusable abstractions in Rust.
Enjoying these tutorials?