Traits
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Rust 1.99.0 and the 2024 edition, checked on 2026-10-06. A trait defines behaviour
that types can share: a set of methods (and associated types and constants) that a type promises to provide. Traits
are Rust’s mechanism for abstraction and polymorphism — they play the role of interfaces, type classes and, combined
with generics, of templates. Static dispatch through generics is covered here; dynamic dispatch with dyn Trait is on
Trait Objects & dyn Compatibility.
Defining and implementing traits
pub trait Describe {
fn title(&self) -> String; // required method
fn summary(&self) -> String { // default method: implementors may override
format!("{} (no summary)", self.title())
}
}
struct Book {
title: String,
year: u16,
}
struct Journal {
name: String,
issue: u32,
}
impl Describe for Book {
fn title(&self) -> String {
self.title.clone()
}
fn summary(&self) -> String {
format!("{}, published {}", self.title, self.year)
}
}
impl Describe for Journal {
fn title(&self) -> String {
format!("{} #{}", self.name, self.issue)
} // uses the default summary
}
fn main() {
let b = Book { title: "Dune".into(), year: 1965 };
let j = Journal { name: "Shelf Quarterly".into(), issue: 12 };
println!("{}\n{}", b.summary(), j.summary());
}
Trait bounds and impl Trait
Generic code constrains type parameters with traits. There are three equivalent ways to write the bound for a parameter, and one more for return types:
use std::fmt::Display;
trait Describe {
fn title(&self) -> String;
}
impl Describe for &str {
fn title(&self) -> String {
self.to_string()
}
}
fn print_a<T: Describe>(item: &T) { // inline bound
println!("{}", item.title());
}
fn print_b<T>(item: &T)
where
T: Describe, // where clause
{
println!("{}", item.title());
}
fn print_c(item: &impl Describe) { // impl Trait in argument position
println!("{}", item.title());
}
fn label(year: u16) -> impl Display { // impl Trait in return position
format!("published {year}")
}
fn main() {
print_a(&"Dune");
print_b(&"Emma");
print_c(&"Kindred");
println!("{}", label(1965));
}
Source: The Book: Traits as Parameters
-
impl Traitin argument position is sugar for an anonymous type parameter; callers cannot name it with a turbofish. -
impl Traitin return position (RPIT) means "some single concrete type that implements the trait, chosen by the function". It hides the type — essential for returning closures and iterator chains, whose types cannot be written — but a function cannot return different types from different branches this way (use an enum orBox<dyn Trait>). Lifetime capture rules for RPIT are on Lifetimes. -
Multiple bounds use
+:T: Describe + Clone,impl Iterator<Item = u32> + Send.
A missing bound is error E0277 ("the trait bound T: Describe is
not satisfied"), the most common trait error, and the message usually says which bound to add.
Supertraits
A trait can require that implementors also implement other traits: trait Catalog: Describe + Debug. Inside the trait
(and for any T: Catalog), methods of the supertraits are available.
Associated types versus generic parameters
An associated type is a type chosen by each implementation, declared with type Name;. Iterator is the classic
example: each iterator has exactly one Item type. A generic parameter on the trait (trait From<T>) instead allows
a type to implement the trait many times, once per T.
Associated type (trait Store { type Key; }) |
Generic parameter (trait Store<Key>) |
|
|---|---|---|
Implementations per type |
one |
many (one per parameter) |
Caller writes |
|
|
Use when |
the type is determined by the implementor |
the implementor works with several types |
Std examples |
|
|
use std::collections::HashMap;
use std::hash::Hash;
trait Repository {
type Key: Hash + Eq + Clone; // associated type with bounds
type Item;
const NAME: &'static str; // associated constant
fn get(&self, key: &Self::Key) -> Option<&Self::Item>;
fn insert(&mut self, key: Self::Key, item: Self::Item);
}
struct BookRepo(HashMap<String, String>);
impl Repository for BookRepo {
type Key = String;
type Item = String;
const NAME: &'static str = "books";
fn get(&self, key: &String) -> Option<&String> {
self.0.get(key)
}
fn insert(&mut self, key: String, item: String) {
self.0.insert(key, item);
}
}
fn fetch_or_default<R: Repository>(repo: &R, key: &R::Key, default: R::Item) -> R::Item
where
R::Item: Clone,
{
repo.get(key).cloned().unwrap_or(default)
}
fn main() {
let mut repo = BookRepo(HashMap::new());
repo.insert("978".into(), "Dune".into());
println!("{} {}", BookRepo::NAME, fetch_or_default(&repo, &"000".to_string(), "unknown".into()));
}
Source: The Book: Associated Types
Generic associated types (GATs)
Since Rust 1.65, associated types can have their own generic parameters, including lifetimes. The canonical use is a
"lending" pattern where the returned type borrows from self, which a plain associated type cannot express:
trait Shelf {
type Titles<'a>: Iterator<Item = &'a str>
where
Self: 'a;
fn titles<'a>(&'a self) -> Self::Titles<'a>;
}
struct VecShelf(Vec<String>);
impl Shelf for VecShelf {
type Titles<'a> = std::iter::Map<std::slice::Iter<'a, String>, fn(&String) -> &str>;
fn titles<'a>(&'a self) -> Self::Titles<'a> {
self.0.iter().map(String::as_str as fn(&String) -> &str)
}
}
fn main() {
let s = VecShelf(vec!["Dune".into(), "Emma".into()]);
println!("{:?}", s.titles().collect::<Vec<_>>());
}
In many cases, returning impl Iterator<Item = &str> + '_ from a trait method (RPITIT, below) is now simpler than a
GAT; GATs remain necessary when callers must name the type or bound it.
Blanket implementations
An impl<T: Bound> Trait for T implements a trait for every type that satisfies a bound. The standard library uses
this for impl<T: Display> ToString for T — which is why every displayable type has .to_string() — and for
impl<T, U: From<T>> Into<U> for T.
use std::fmt::Display;
trait Shout {
fn shout(&self) -> String;
}
impl<T: Display + ?Sized> Shout for T {
fn shout(&self) -> String {
self.to_string().to_uppercase() + "!"
}
}
fn main() {
println!("{} {} {}", "dune".shout(), 1965.shout(), 'x'.shout());
}
Coherence and the orphan rule
Rust guarantees coherence: for any type and trait, there is at most one implementation in the whole program, so the compiler never has to choose. Two rules enforce it:
-
No overlap: two impls that could apply to the same type are rejected (error E0119, "conflicting implementations"). Specialisation, which would relax this, is nightly-only.
-
The orphan rule: you may implement a trait for a type only if the trait or the type is defined in your crate (more precisely, a local type must appear before any uncovered type parameter). Implementing
DisplayforVec<T>in your crate is error E0117.
use std::fmt;
impl fmt::Display for Vec<String> { // error[E0117]: only traits defined in the current crate can be implemented for types defined outside of the crate
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.join(", "))
}
}
fn main() {}
The orphan rule is what lets two crates be combined without conflicting impls. The standard workaround is a newtype — a local wrapper type for which you may implement anything:
use std::fmt;
struct TitleList(Vec<String>);
impl fmt::Display for TitleList {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0.join(", "))
}
}
impl std::ops::Deref for TitleList { // optional: expose the inner Vec's methods (use sparingly)
type Target = Vec<String>;
fn deref(&self) -> &Vec<String> {
&self.0
}
}
fn main() {
let list = TitleList(vec!["Dune".into(), "Emma".into()]);
println!("{list} ({} titles)", list.len());
}
Sealed traits
A public trait can be sealed so that downstream crates can use it but not implement it, leaving the author free to add methods later without a breaking change. The idiom is a supertrait in a private module:
mod format {
mod private {
pub trait Sealed {}
}
/// Output formats supported by the exporter. This trait is sealed.
pub trait ExportFormat: private::Sealed {
fn extension(&self) -> &'static str;
}
pub struct Json;
pub struct Csv;
impl private::Sealed for Json {}
impl private::Sealed for Csv {}
impl ExportFormat for Json {
fn extension(&self) -> &'static str {
"json"
}
}
impl ExportFormat for Csv {
fn extension(&self) -> &'static str {
"csv"
}
}
}
use format::ExportFormat;
fn file_name(base: &str, f: &impl ExportFormat) -> String {
format!("{base}.{}", f.extension())
}
fn main() {
println!("{} {}", file_name("books", &format::Json), file_name("books", &format::Csv));
}
Fully qualified syntax
When two traits (or a trait and an inherent impl) define a method with the same name, a method call is ambiguous
(error E0034). Call through the trait explicitly with
Trait::method(&value), or with fully qualified syntax <Type as Trait>::function() for associated functions that
have no self:
trait Printable {
fn label() -> String;
fn id(&self) -> String;
}
trait Shelvable {
fn id(&self) -> String;
}
struct Book;
impl Book {
fn id(&self) -> String {
"inherent".into()
}
}
impl Printable for Book {
fn label() -> String {
"book".into()
}
fn id(&self) -> String {
"isbn".into()
}
}
impl Shelvable for Book {
fn id(&self) -> String {
"shelf code".into()
}
}
fn main() {
let b = Book;
println!("{}", b.id()); // inherent method wins
println!("{}", Printable::id(&b)); // trait method
println!("{}", <Book as Shelvable>::id(&b)); // fully qualified
println!("{}", <Book as Printable>::label()); // associated function without self
}
The Catalog trait hierarchy in shelf
shelf-core defines a small hierarchy that the CLI and the web application share. Supertraits add requirements;
the in-memory implementation and a database-backed one implement the same traits:
async fn and impl Trait in traits
Since Rust 1.75, trait methods can be async fn and can return impl Trait (return-position impl Trait in traits,
RPITIT). Each implementation’s future or iterator type is an anonymous associated type:
use std::collections::HashMap;
trait AsyncCatalog {
async fn title(&self, isbn: &str) -> Option<String>;
fn all_titles(&self) -> impl Iterator<Item = &str>;
}
struct MemCatalog(HashMap<String, String>);
impl AsyncCatalog for MemCatalog {
async fn title(&self, isbn: &str) -> Option<String> {
self.0.get(isbn).cloned() // a real impl would await a database here
}
fn all_titles(&self) -> impl Iterator<Item = &str> {
self.0.values().map(String::as_str)
}
}
#[tokio::main(flavor = "current_thread")]
async fn main() {
let c = MemCatalog(HashMap::from([("978".to_string(), "Dune".to_string())]));
println!("{:?} {:?}", c.title("978").await, c.all_titles().collect::<Vec<_>>());
}
Source: The Rust Reference: Traits
Two limitations remain. Callers cannot add a Send bound to the returned future, so generic code that spawns it on a
multi-threaded runtime cannot be written directly against such a trait (the async_fn_in_trait lint warns about this
for public traits); the workaround is to declare the method as fn title(&self, ..) -> impl Future<Output = ..> + Send.
And traits with async fn or RPITIT are not dyn-compatible — see
Trait Objects & dyn Compatibility for the
async-trait crate and boxing workarounds, and Async/Await
& Futures.
Diagnostic attributes
Library authors can improve the E0277 message users see when a type does not implement a trait.
#[diagnostic::on_unimplemented] (1.78) customises the message, label and notes; #[diagnostic::do_not_recommend]
(1.85) on an impl stops the compiler from suggesting that impl in errors:
#[diagnostic::on_unimplemented(
message = "`{Self}` cannot be stored in the catalogue",
label = "this type does not implement `Catalogable`",
note = "derive or implement `Catalogable` for `{Self}`"
)]
trait Catalogable {
fn key(&self) -> String;
}
impl Catalogable for String {
fn key(&self) -> String {
self.clone()
}
}
fn store<T: Catalogable>(item: T) -> String {
item.key()
}
fn main() {
println!("{}", store(String::from("Dune")));
// store(42); // E0277 with the custom message: `{integer}` cannot be stored in the catalogue
}
Traits compared with interfaces
Traits resemble Java and C# interfaces (see Java: Interfaces and C#: Interfaces), with differences that matter in practice:
-
implementations are separate from the type definition, so you can implement your trait for existing types (
impl Describe for String), within the orphan rule; -
there is no inheritance of data — supertraits only add requirements;
-
generic code is monomorphised (static dispatch) by default; dynamic dispatch is opt-in with
dyn; -
traits can have associated types, constants and static functions, and can be implemented conditionally (blanket impls).
Where the books are outdated
Azam (Advanced Rust, chapter 7) covers associated types versus generic parameters, the orphan rule and newtypes, and
sealed traits, in line with the Reference. Since then, async fn and RPITIT in traits (1.75), the diagnostic attribute
namespace (1.78, 1.85) and trait upcasting (1.86) have been stabilised; examples that use the async-trait macro for
static dispatch are no longer necessary.
References
-
The Rust Programming Language: Defining Shared Behavior with Traits
-
Rust by Example: Traits (derive, impl Trait, supertraits, disambiguating)