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.
Traits: Defining Shared Behavior
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:
- Defining a Trait – The syntax and semantics of creating a trait with method signatures.
- Implementing a Trait on a Type – How to make your own types (or types defined in your crate) conform to a trait.
- Default Implementations – Providing fallback method bodies that types can use or override, reducing boilerplate.
- Traits as Parameters and Return Types – Using
impl Traitand generic parameters to write functions that work with any type that implements a trait. - Trait Bounds and
whereClauses – Writing complex constraints clearly, and understanding how bounds propagate through your code. - Using Traits – Bringing traits into scope, calling trait methods, and leveraging trait‑bound APIs from the standard library.
- Trait Objects and Dynamic Dispatch – Using
dyn Traitto store and pass values of different types that share a trait, at a small runtime cost. - Generic Functions vs Trait Objects (Trade‑offs) – Choosing between compile‑time polymorphism and runtime flexibility.
- Reverse-Engineering Bounds from Type Requirements – A technique for discovering the minimal trait bounds a generic function needs by following what the compiler asks for.
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.