DEV Community

Subesh Yadav
Subesh Yadav

Posted on

🦀 Day 18 of #100DaysOfRust: Deep Dive into Traits in Rust

Today’s learning was focused on Traits—a powerful abstraction mechanism in Rust that lets you define shared behavior. This post covers everything I explored: from basic definitions to conditional and blanket implementations. Let’s break it down step by step with simple explanations and relevant examples.


🔷 What Are Traits?

Traits in Rust define shared behavior across types, similar to interfaces in languages like JavaScript or TypeScript.

A trait contains method signatures that types can implement. If a type implements a trait, it guarantees that the method(s) are defined with the expected behavior.


pub trait Summary {
    fn summarize(&self) -> String;
}
Enter fullscreen mode Exit fullscreen mode

🔧 Implementing Traits on Types

You can implement traits on structs. Let’s take a look at two simple implementations:

pub struct NewsArticle {
    pub headline: String,
    pub location: String,
    pub author: String,
    pub content: String,
}

impl Summary for NewsArticle {
    fn summarize(&self) -> String {
        format!("{}, by {} ({})", self.headline, self.author, self.location)
    }
}
Enter fullscreen mode Exit fullscreen mode

You can similarly implement Summary for other types like a SocialPost.


🚦 Trait Scope Rules

You can only implement a trait on a type if:

  • The trait or the type is local to your crate.
  • You can’t implement external traits on external types due to the orphan rule. This ensures code from different crates doesn’t conflict.

✅ Valid:

impl Display for MyType {} // Display is external, MyType is local.
Enter fullscreen mode Exit fullscreen mode

❌ Invalid:

impl Display for Vec<T> {} // Both are external.
Enter fullscreen mode Exit fullscreen mode

🧰 Default Implementations

Traits can have default method implementations. Types implementing the trait may override or use these defaults.

pub trait Summary {
    fn summarize(&self) -> String {
        String::from("(Read more...)")
    }
}
Enter fullscreen mode Exit fullscreen mode

This reduces boilerplate, especially when common behavior exists.


🧩 Traits Calling Other Trait Methods

You can build compositional behavior in traits:

pub trait Summary {
    fn summarize_author(&self) -> String;

    fn summarize(&self) -> String {
        format!("(Read more from {}...)", self.summarize_author())
    }
}
Enter fullscreen mode Exit fullscreen mode

Then only implement the required piece:

impl Summary for SocialPost {
    fn summarize_author(&self) -> String {
        format!("@{}", self.username)
    }
}
Enter fullscreen mode Exit fullscreen mode

🧬 Traits as Function Parameters

With impl Trait syntax:

pub fn notify(item: &impl Summary) {
    println!("Breaking news! {}", item.summarize());
}
Enter fullscreen mode Exit fullscreen mode

With explicit trait bounds:

pub fn notify<T: Summary>(item: &T) {
    println!("Breaking news! {}", item.summarize());
}
Enter fullscreen mode Exit fullscreen mode

➕ Multiple Trait Bounds

You can require multiple traits using +:

pub fn notify<T: Summary + Display>(item: &T) {
    println!("Info: {}", item);
}
Enter fullscreen mode Exit fullscreen mode

Or using a where clause for better readability:

fn some_function<T, U>(t: &T, u: &U)
where
    T: Display + Clone,
    U: Clone + Debug,
{}
Enter fullscreen mode Exit fullscreen mode

🔁 Returning Types That Implement Traits

You can return types that implement a trait without naming them:

fn returns_summarizable() -> impl Summary {
    SocialPost {
        username: String::from("horse_ebooks"),
        content: String::from("content..."),
        reply: false,
        repost: false,
    }
}
Enter fullscreen mode Exit fullscreen mode

🛑 You can’t return different types under impl Trait (e.g., either NewsArticle or SocialPost)—they must be the same type.


🧠 Conditional Trait Implementation (Generic + Trait Bounds)

You can conditionally implement methods on types that implement specific traits:

impl<T: Display + PartialOrd> Pair<T> {
    fn cmp_display(&self) {
        if self.x >= self.y {
            println!("x is bigger: {}", self.x);
        } else {
            println!("y is bigger: {}", self.y);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

🌐 Blanket Implementations

You can implement a trait for any type that implements another trait:

impl<T: Display> ToString for T {}
Enter fullscreen mode Exit fullscreen mode

This is how types like i32, f64, or even custom types that implement Display get to_string() for free.


✅ Summary

  • Traits allow defining shared behavior like interfaces.
  • You can implement traits for structs and enums.
  • Traits can provide default implementations.
  • Traits can be used as function parameters and return types.
  • You can use bounds and where clauses for cleaner generics.
  • Conditional and blanket implementations enable powerful patterns.
  • Rust enforces safety at compile-time using traits, minimizing runtime surprises.

This wraps up my Day 18 of #100DaysOfRust. Traits are a cornerstone of Rust’s expressive type system. Their power lies in enabling clean, safe, and reusable abstractions.

Top comments (0)