rust-traits-comportamientos-interfaces

Traits in Rust: Sharing Behavior Between Types

  • 4 min

A trait is a contract that defines behaviors other types can implement.

If you come from Java or C#, it’s conceptually similar to an Interface. If you come from C++, it’s similar to an Abstract Class (with pure virtual functions). If you come from Haskell, it’s a Type Class.

In essence, a Trait defines shared functionality. It tells the compiler: “I don’t care what this object is, I only care that it has a method called x.

Defining a Trait

Imagine we’re building a news application. We have Article, Tweet, and FacebookStatus. We want a unified way to ask for a “summary” of each of them.

We define a trait using the trait keyword:

pub trait Summarizable {
    // We declare the method signature, but not the body (usually)
    fn summarize(&self) -> String;
}
Copied!

Any type that wants to be Summarizable must implement this method with that exact signature.

Implementing a Trait on a Type

Now let’s implement this behavior for two different structs. The syntax is: impl TraitName for TypeName.

pub struct Article {
    pub title: String,
    pub author: String,
    pub content: String,
}

impl Summarizable for Article {
    fn summarize(&self) -> String {
        format!("{} by {}", self.title, self.author)
    }
}

pub struct Tweet {
    pub username: String,
    pub content: String,
}

impl Summarizable for Tweet {
    fn summarize(&self) -> String {
        format!("{}: {}", self.username, self.content)
    }
}
Copied!

Now, even though Article and Tweet are completely different types, they both share a behavior.

fn main() {
    let tweet = Tweet {
        username: String::from("rust_lang"),
        content: String::from("Traits are awesome!"),
    };

    println!("New tweet: {}", tweet.summarize());
}
Copied!

Default Implementations

Unlike old Java interfaces, Traits in Rust can have code. We can provide a default behavior so types don’t have to implement it mandatorily if they don’t want to.

pub trait Summarizable {
    // Default implementation
    fn summarize(&self) -> String {
        String::from("(Read more...)")
    }
}
Copied!

If we now create a struct and leave the impl block empty, it will use the default:

struct Photo {
    url: String,
}

impl Summarizable for Photo {} // Empty block = use default

fn main() {
    let p = Photo { url: String::from("img.jpg") };
    println!("{}", p.summarize()); // Prints: (Read more...)
}
Copied!

Default implementations can call other methods of the same trait, even if those other methods don’t have default implementations. This allows creating “template methods”.

Traits as Parameters

Traits combine with functions to give us polymorphism. We can write a function that accepts anything that implements the Summarizable trait.

// "impl Trait" syntax (Syntactic sugar)
pub fn notify(item: &impl Summarizable) {
    println!("Breaking news: {}", item.summarize());
}

fn main() {
    let article = Article { ... };
    let tweet = Tweet { ... };

    notify(&article); // ✅ Works
    notify(&tweet);    // ✅ Works
}
Copied!

The notify function doesn’t know if you’re passing it a Tweet or an Article. It only knows it can call .summarize().

The Orphan Rule

There’s an important restriction in Rust on where you can implement a trait. To implement a trait on a type, at least one of the two must be local to your Crate (your project).

  • ✅ You can implement your Summarizable trait on your Tweet type.
  • ✅ You can implement your Summarizable trait on the external type String (from the standard library).
  • ✅ You can implement the external trait Display (from std) on your Tweet type.
  • ❌ You CANNOT implement the external trait Display on the external type String.
// ❌ Error: Impl of external trait for external type
impl std::fmt::Display for String { ... }
Copied!

This rule prevents incompatible implementations. Without it, two libraries could define different ways to print a String and there would be no consistent choice when both participate in the same program.