Using Traits
Learn how to call trait methods, write generic functions with trait bounds, and understand how static dispatch works in Rust.
When you define a trait and implement it on some types, you have described shared behavior. Actually using that behavior in your code means calling trait methods, bringing the trait into scope, and—most importantly—writing functions that operate on any type that satisfies the trait, without knowing which concrete type they will receive until compile time. In Rust, the primary way to use traits for this kind of polymorphism is through generics with trait bounds, which gives you static dispatch: the compiler knows exactly which function to call, so there is no runtime overhead.
This page focuses on how to use traits in practice: calling methods, the different syntaxes for expressing trait requirements on generics, and what the compiler does with that information. The complementary approach of using trait objects for dynamic dispatch is covered in the next section.
Bringing a Trait into Scope
A trait method is only available on a value if the trait itself is in scope, even when the type is already visible. Rust imposes this rule to keep method resolution predictable—otherwise importing a type could silently add dozens of methods, making name collisions likely.
Consider a crate that defines a Summary trait and implements it for NewsArticle:
pub trait Summary {
fn summarize(&self) -> String;
}
pub struct NewsArticle {
pub headline: String,
pub content: String,
}
impl Summary for NewsArticle {
fn summarize(&self) -> String {
format!("{} ...", self.headline)
}
}
A binary crate that uses this library will get a compilation error if it tries to call summarize directly without importing the trait:
use aggregator::NewsArticle; // Trait not imported
fn main() {
let article = NewsArticle {
headline: String::from("Rust 1.85 Released"),
content: String::from("..."),
};
println!("{}", article.summarize());
}
Compile Error — Trait Not in Scope:
The compiler will emit an error like: no method named 'summarize' found for struct 'NewsArticle'. You must bring the Summary trait into scope with use aggregator::Summary;.
After adding the import, the call works:
use aggregator::{NewsArticle, Summary};
fn main() {
let article = NewsArticle {
headline: String::from("Rust 1.85 Released"),
content: String::from("..."),
};
println!("{}", article.summarize());
}
The standard library prelude automatically imports a handful of frequently used traits like Into, From, Fn, and Iterator. For everything else—traits from external crates or your own modules—you must add an explicit use statement.
Calling Trait Methods on Concrete Types
Once the trait is in scope, calling a trait method looks identical to calling an inherent method. The same struct can have both inherent impl blocks and trait impl blocks; the dot syntax works seamlessly for both.
use aggregator::{NewsArticle, SocialPost, Summary};
let article = NewsArticle {
headline: String::from("Penguins win the Stanley Cup!"),
content: String::from("..."),
// ...
};
let post = SocialPost {
username: String::from("horse_ebooks"),
content: String::from("of course, as you probably already know, people"),
reply: false,
repost: false,
};
println!("Article: {}", article.summarize());
println!("Post: {}", post.summarize());
The calls look the same, but each dispatches to the implementation defined for that concrete type—NewsArticle::summarize in the first call and SocialPost::summarize in the second. The compiler knows the exact types, so this is resolved at compile time.
Using Traits to Write Generic Functions
The real leverage comes when you stop writing functions that accept a single concrete type and instead write generic functions that accept any type implementing a trait. The trait bound tells the compiler: “I only need these specific behaviors from T; accept anything that provides them.”
Imagine printing a summary for any type that implements Summary. The function signature expresses this requirement in one of three equivalent forms.
fn print_summary<T: Summary>(item: &T) {
println!("Summary: {}", item.summarize());
}
All three signatures compile to exactly the same binary. The impl Trait form in argument position is syntactic sugar for <T: Trait>, and the where clause is syntactic sugar that separates the constraint list from the generic parameter list—especially helpful when you have several traits to specify.
When to Prefer the Full Generic Syntax:
impl Trait cannot be used when you need to express that two parameters share the same concrete type. If a function takes two arguments that must both be the same kind of thing, you need a named generic parameter like <T: Summary> so you can write (a: &T, b: &T). With impl Summary, each parameter could be a different type.
Now you can pass any type that implements Summary:
let article = NewsArticle { /* ... */ };
let post = SocialPost { /* ... */ };
print_summary(&article);
print_summary(&post);
When the compiler encounters these calls, it sees the concrete types NewsArticle and SocialPost and generates a separate copy of print_summary for each. Inside the NewsArticle copy, item.summarize() directly calls NewsArticle::summarize(). Inside the SocialPost copy, it calls SocialPost::summarize(). There is no indirection, no runtime type check, and no performance penalty compared to writing separate functions by hand.
What the Compiler Does with Trait Bounds
This process is called monomorphization—the same mechanism used for generic types without trait bounds. The compiler replaces every generic parameter with the concrete type used at each call site, generating a monomorphic (single-type) version of the function for each distinct combination of type arguments.
A function like fn show\u003cT: Display\u003e(val: T) called with String and i32 results in two functions behind the scenes:
show<String> → fn show(val: String) { /* Display impl for String */ }
show\u003ci32\u003e → fn show(val: i32) { /* Display impl for i32 */ }
The trait bound guarantees that every concrete type used will have the required method, so the generated code can call it directly. This is static dispatch: the call target is fixed at compile time.
The trade-off is binary size. Each new type combination creates an additional copy of the function body. In most applications this is negligible, but it is worth knowing when you work with many distinct types or deeply nested generics.
Zero-Cost Abstraction:
This is one of Rust's signature design wins. The abstraction of "anything that can be displayed" compiles away entirely, leaving code that runs as fast as if you had written a handwritten version for each type.
Using Traits in Return Types (A Quick Look)
Traits can also appear in return types using impl Trait:
fn make_summarizable() -> impl Summary {
SocialPost {
username: String::from("bot"),
content: String::from("hello"),
reply: false,
repost: false,
}
}
The function signature says: “I will return some type that implements Summary, but I won't tell you which.” Callers can only use the trait methods; they cannot rely on any specifics of the concrete type. The compiler still knows the real type at compile time, so this is static dispatch—the function must return a single, consistent type for all code paths inside the body. Returning different types depending on a runtime condition is not allowed with impl Trait in return position; that scenario requires trait objects, discussed in the next section.
Common Pitfalls and How to Avoid Them
Forgetting to import the trait is the most frequent beginner mistake, but a few other snags appear when writing generic functions with traits.
Losing Type Equality with impl Trait
Using impl Trait for multiple arguments can lead to a subtle loss of constraint. If you want both arguments to have exactly the same type, the impl Trait shorthand will accept different types that both implement the trait, which may be unintended.
// Accepts two potentially different types.
fn pair_up(a: &impl Summary, b: &impl Summary) {
// ...
}
// Accepts exactly one type.
fn pair_up\u003cT: Summary\u003e(a: &T, b: &T) {
// ...
}
Different Types Silently Allowed:
When you pass a NewsArticle and a SocialPost to the first version, it compiles without complaint because each argument's impl Summary can resolve to a different type. The second version would reject that call with a type mismatch error. If your logic assumes both arguments represent the same kind of entity, use the named generic parameter.
Thinking Traits Add Runtime Cost
Trait bounds on generics add zero runtime overhead. It is common for newcomers, especially from languages where interfaces involve vtables, to assume that “accepting a trait” always means dynamic dispatch. In Rust, the default pattern—generic functions with trait bounds—is fully monomorphized and static. Dynamic dispatch only enters the picture when you explicitly opt into &dyn Trait or Box\u003cdyn Trait\u003e.
Over-Constraining a Generic
Not every function needs a trait bound. If the function body never calls a trait method, adding a bound is unnecessary and reduces flexibility. Start with the constraints the body actually requires; the compiler will tell you exactly what is missing.
Summary
Using traits in Rust revolves around one idea: write functions that talk about behavior, not concrete types. By combining generics with trait bounds, you get polymorphic code that the compiler monomorphizes into direct, single-type functions—no indirection, no performance cost. The three syntaxes (\u003cT: Trait\u003e, impl Trait in argument position, and where clauses) are interchangeable for static dispatch; choose the one that makes your code most readable for the situation.
The distinction between static dispatch via generics and dynamic dispatch via dyn Trait is one of the central design choices in Rust. Understanding both arms you with the full range of tools for writing expressive, performant code.