Web Analytics

Traits - Defining Shared Behavior

Advanced ~35 min read

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;
}
Think of Traits Like: Interfaces in Java/C#, protocols in Swift, or type classes in Haskell. They define a contract that types must fulfill.

Defining and Implementing Traits

Rust Traits
Output
Click Run to execute your code

Default Implementations

Traits can provide default method implementations that types can use or override:

Output
Click Run to execute your code
Pro Tip: Default implementations can call other trait methods, even if they don't have default implementations. This allows you to build complex behavior from simple requirements.

Traits as Parameters

You can use traits to accept multiple types that implement the same trait:

Output
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

Output
Click Run to execute your code
Important: When using 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.

Output
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.