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());
}

An impl must provide every required item (error E0046) and nothing that the trait does not declare (error E0407). To call a trait method, the trait must be in scope (use); otherwise the method is not found (E0599) and the compiler suggests the import.

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));
}
  • impl Trait in argument position is sugar for an anonymous type parameter; callers cannot name it with a turbofish.

  • impl Trait in 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 or Box<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

T: Store (then T::Key)

T: Store<Isbn>

Use when

the type is determined by the implementor

the implementor works with several types

Std examples

Iterator::Item, Deref::Target, Add::Output

From<T>, AsRef<T>, Add<Rhs>

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()));
}

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 Display for Vec<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:

classDiagram class Describe { <<trait>> +title() String +summary() String } class Catalog { <<trait>> +add(book) Result +get(isbn) Option +search(query) Vec +lend(isbn, member) Result +give_back(isbn) Result } class AsyncCatalog { <<trait>> +get(isbn) Future +search(query) Future } class InMemoryCatalog { -books HashMap -loans HashMap } class PgCatalog { -pool PgPool } class Book Describe <|.. Book : implements Catalog <|.. InMemoryCatalog : implements AsyncCatalog <|.. PgCatalog : implements Catalog ..> Book : stores

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<_>>());
}

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.