R
RUSTY YELLOW PAGES

traitKeyword


Explanation

trait declares a named set of behavior: method signatures a type can promise to provide, as in trait Greet { fn greet(&self) -> String; }. A trait by itself defines no data and no memory layout — it's a contract, implemented for a concrete type separately via impl.

Inside the braces, a trait body can hold:

  • Method signatures with no body (fn greet(&self) -> String;) — required, every implementer must supply one.
  • Method signatures with a default body (fn greet(&self) -> String { ... }) — optional to override; see Default trait methods.
  • Associated types (type Item;) — a placeholder type each implementer fixes to something concrete, as Iterator does with type Item;.
  • Associated consts (const MAX: u32;) — a named constant each implementer supplies, resolved per-implementation rather than shared.

A trait can also declare supertraits with a : after its name — trait Sub: Super requires any Sub implementer to also implement Super, guaranteeing Super's methods are available inside Sub's own default bodies. See Supertraits for why this exists and what it buys you; the : here is the same trait bound syntax used on generic parameters, just attached to a trait declaration instead of a type parameter.

Usage examples

Declaring a trait's method contract

trait Greet { // <- `trait` declares the contract, no implementation yet
    fn greet(&self) -> String;
}

Implementing traits

A trait that mixes one required method with one default method lets every implementer supply only the irreducible part, while sharing the rest.

trait Greet {
    fn name(&self) -> String; // <- required: no body, every implementer must define it
    fn greet(&self) -> String { // <- optional: default body, inherited unless overridden
        format!("Hello, {}!", self.name())
    }
}

struct Visitor;
impl Greet for Visitor {
    fn name(&self) -> String { "Visitor".into() } // greet() comes for free
}

println!("{}", Visitor.greet());

Declaring name as required and greet as a default lets the trait capture exactly one piece of per-type information while generating the rest, which the Rust Book covers as the standard shape for a trait that's more than a bare method list.

Designing a public API

A crate that wants callers to plug in their own storage backend declares a narrow trait as the extension point, rather than exposing an internal struct for callers to depend on directly.

pub trait Storage { // <- `trait` is the extension point; callers implement it for their own types
    fn get(&self, key: &str) -> Option<String>;
    fn set(&mut self, key: &str, value: String);
}

pub struct KeyValueStore<S: Storage> {
    backend: S,
}

impl<S: Storage> KeyValueStore<S> {
    pub fn refresh(&mut self, key: &str, value: String) {
        self.backend.set(key, value);
    }
}

Keeping the trait's method list minimal leaves room to add backends later without breaking existing implementers — see Traits for the fuller design rationale behind shipping a trait as an API boundary.

Explanation

Embedded support: Full

This is where trait earns its central place in embedded Rust, rather than merely still working under #![no_std]. embedded-hal is a set of traits — OutputPin, InputPin, SpiBus, I2c, DelayNs, and others — with no implementation code of its own, only method signatures a peripheral must support. A chip vendor's HAL crate (commonly built atop a peripheral-access crate generated by svd2rust from that chip's SVD file) implements these traits for its own concrete peripheral types; a driver crate for a sensor or display is then written generically against the trait, never against any one vendor's type. The payoff is that the same driver crate compiles and runs unmodified against any microcontroller whose HAL implements the traits it needs — the trait is the abstraction boundary that keeps a driver hardware-agnostic, letting one sensor or display driver crate serve an STM32 board and an RP2040 board without a single vendor-specific line inside it.

Usage examples

Declaring an embedded-hal-style trait

trait OutputPin { // <- `trait`: the contract every vendor's GPIO pin type must satisfy
    type Error;
    fn set_high(&mut self) -> Result<(), Self::Error>;
    fn set_low(&mut self) -> Result<(), Self::Error>;
}

Writing a hardware-agnostic driver against the trait

struct Led<P: OutputPin> { // <- generic over any type implementing OutputPin, not one vendor's pin type
    pin: P,
}

impl<P: OutputPin> Led<P> {
    fn turn_on(&mut self) -> Result<(), P::Error> {
        self.pin.set_high()
    }
}

This Led driver compiles against any microcontroller's HAL that implements OutputPin for its own pin type — swapping boards means swapping which concrete type fills in P, with no change to Led itself.