Traits - Defining Shared Behavior

Learn how Rust traits define shared behavior across types, why they exist, and how they power generics, abstraction, and polymorphism in your code.

Rust’s type system gives you the power to write code that works with many different types without sacrificing safety. One of the two pillars that make this possible—alongside generics—is traits. A trait is a collection of method signatures that a type can choose to implement. When you define a trait, you are defining a contract: any type that signs it guarantees to provide certain behaviors.

This section introduces you to that contract. We’ll start with what traits are and why Rust needs them, then walk through each major aspect of working with traits, from defining one to using them for both compile-time and runtime polymorphism.

The Problem Traits Solve

Imagine you’re writing a media library. You have a NewsArticle struct and a SocialPost struct. They store completely different data, but you want a single way to get a summary from either. Without traits, you’d write two entirely separate functions or resort to stringly‑typed workarounds that lose all type safety.

Traits let you say: “I expect any type that can be summarized to have a summarize method that returns a String.” Once you’ve expressed that expectation, you can write functions that accept anything meeting it—without caring about what concrete type sits behind the scenes.

This is abstraction over behavior, not over data. It’s what makes generic code expressive and still entirely type‑checked at compile time.

Traits vs interfaces:

If you’ve used interfaces in languages like Java or Go, traits will feel familiar. The key difference is that Rust traits can contain not just method signatures but also default implementations, associated types, and static methods—and they can be used to constrain generic parameters in powerful ways.

What a Trait Looks Like

A trait definition groups method signatures. It uses the trait keyword and lists the required methods, each ending with a semicolon instead of a body:

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

Any type that wants to be Summary‑capable must provide exactly this method with the exact same signature. Here’s a NewsArticle that fulfills the contract:

pub struct NewsArticle {
    pub headline: String,
    pub author: String,
    pub location: String,
    pub content: String,
}
impl Summary for NewsArticle {
    fn summarize(&self) -> String {
        format!("{}, by {} ({})", self.headline, self.author, self.location)
    }
}

The impl TraitName for Type block tells the compiler, “Here is the concrete behavior for this type.” The method body is up to you—as long as the signature matches, you can decide what a summary means for each type.

Once implemented, you call trait methods just like any other method, as long as you’ve brought the trait into scope:

use aggregator::Summary;
let article = NewsArticle {
    // ... fields ...
};
println!("New article: {}", article.summarize());

The compiler enforces that every type implementing Summary has summarize. There’s no way to accidentally forget it or spell it wrong—your code won’t compile.

Trait methods are ordinary methods:

After implementing a trait, you call its methods on instances exactly the same way you call inherent methods. The only difference is that you must import the trait if you’re using it from another module or crate. If your code compiles, all required methods are present.

How Traits Work with Generics

Traits truly shine when paired with generics. A generic function like fn largest<T>(list: &[T]) -> T can accept any type T, but it can’t do anything useful because it doesn’t know what operations T supports. Add a trait bound, and the generic gains constraints:

fn largest<T: PartialOrd + Copy>(list: &[T]) -> T {
    let mut largest = list[0];
    for &item in list {
        if item > largest {
            largest = item;
        }
    }
    largest
}

The bound T: PartialOrd + Copy tells the compiler: “T must be a type that can be compared and trivially copied.” Now the function body can safely use > and assign values without moving them. This is static dispatch: the compiler generates a separate version of largest for each concrete type you use, resulting in zero runtime overhead.

Traits can also be used as function parameters directly via the impl Trait syntax:

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

This is syntactic sugar for a generic with a trait bound. It means the same thing but keeps the function signature shorter. In the next pages, you’ll see when to prefer which syntax.

Key Concepts Covered in This Section

Each aspect of traits builds on the last. Here’s what you’ll find as you progress:

Each topic is covered in its own deep dive, but the concepts are tightly interwoven. Don’t worry if you need to jump back and forth—the goal is to give you the mental models to choose the right tool for each situation.

Important Rules and Gotchas

Rust enforces a few rules to keep traits predictable and safe across crates.

The orphan rule:

You can implement a trait on a type only if at least one of the trait or the type is local to your crate. This means you cannot implement a standard library trait (like Display) on a standard library type (like Vec<T>) from your own crate. This restriction prevents two crates from supplying conflicting implementations and ensures that trait resolution is unambiguous.

Default implementations can hide missing methods:

A trait with default implementations can compile even if a type’s impl block is empty. If you later add a new required method to the trait, all existing implementations will break until they provide that method. Keep that in mind when evolving a public trait.

Trait methods are in scope only when the trait is imported:

Even if a type implements a trait, you can’t call the trait’s methods unless the trait is in scope. This is a deliberate design choice: it allows multiple traits to have methods with the same name without ambiguity. If you get a “method not found” error, import the trait.

Defining a Trait

Understand the syntax and purpose of trait definitions in Rust—how to declare shared method signatures that establish a contract for behavior across types.

Implementing a Trait on a Type

How to provide concrete behavior for a trait on a struct or enum in Rust covering the impl syntax, the orphan rule, scope imports, and common pitfalls

Default Implementations

How default methods in Rust traits reduce boilerplate by providing shared behavior you can override or accept as-is

Traits as Parameters and Return Types

How to use traits in function signatures to accept any type that implements a trait and to return opaque types

Trait Bounds and where Clauses

Learn how to constrain generic types in Rust using trait bounds, the impl Trait sugar, multiple bounds with +, and the where clause for readable, complex signatures.

Using Traits

Learn how to call trait methods, write generic functions with trait bounds, and understand how static dispatch works in Rust.

Trait Objects and Dynamic Dispatch

Understand how trait objects enable runtime polymorphism in Rust, how dynamic dispatch works through vtables, and when to use dyn Trait instead of generics.

Generic Functions vs Trait Objects (Trade-offs)

A thorough comparison of static dispatch with generic functions and dynamic dispatch with trait objects, covering performance, binary size, collections, object safety, and when to choose each.

Reverse-Engineering Bounds from Type Requirements

A practical guide to deducing trait bounds by examining what operations you perform on generic types in Rust, not by memorizing lists.