Skip to main content
Rust · lesson 14 of 30
Rust intermediate Lesson 14 of 30

Traits in Rust

Define and implement Rust traits: default methods, trait bounds, impl Trait vs dyn Trait, From/Into, operator overloading, supertraits, object safety.

By SkillByExample · Updated

Defining a Trait

Traits are Rust’s primary abstraction mechanism. A trait declares a set of method signatures that any conforming type must provide, letting you write code that works across many different types without knowing which specific type you’ll receive. This is how the standard library achieves polymorphism without inheritance — Display, Iterator, Clone, and hundreds of other behaviors are all traits.

trait Greet {
    fn hello(&self) -> String;

    // Default implementation — types can override this, but don't have to
    fn goodbye(&self) -> String {
        String::from("Goodbye!")
    }
}

struct English;
struct Spanish;

impl Greet for English {
    fn hello(&self) -> String {
        String::from("Hello!")
    }
}

impl Greet for Spanish {
    fn hello(&self) -> String {
        String::from("¡Hola!")
    }

    // Override the default goodbye
    fn goodbye(&self) -> String {
        String::from("¡Adiós!")
    }
}

fn main() {
    let e = English;
    let s = Spanish;

    println!("{}", e.hello());
    println!("{}", e.goodbye()); // uses the default
    println!("{}", s.hello());
    println!("{}", s.goodbye()); // overridden
}
$ rustc traits_defining_a_trait.rs && ./traits_defining_a_trait
Hello!
Goodbye!
¡Hola!
¡Adiós!

Trait Bounds on Functions

Trait bounds let you write functions that accept any type implementing a given trait, rather than a single concrete type. This is how you get the benefits of generics while still constraining what operations are available. The compiler generates a specialized version of the function for each concrete type used — zero runtime overhead.

use std::fmt::Display;

// Syntax 1: inline bound — T must implement both PartialOrd and Display
fn print_largest<T: PartialOrd + Display>(list: &[T]) {
    let mut largest = &list[0];
    for item in list {
        if item > largest {
            largest = item;
        }
    }
    println!("largest: {}", largest);
}

// Syntax 2: where clause — easier to read when bounds are complex
fn print_pair<T, U>(a: T, b: U)
where
    T: Display + Clone,
    U: Display,
{
    println!("{} and {}", a.clone(), b);
}

fn main() {
    print_largest(&[34, 50, 25, 100, 65]); // largest: 100
    print_largest(&[1.5, 2.3, 0.8, 9.1]);  // largest: 9.1
    print_pair("hello", 42);
}
$ rustc traits_trait_bounds_on_functions.rs && ./traits_trait_bounds_on_functions
largest: 100
largest: 9.1
hello and 42

impl Trait Syntax — Static Dispatch

impl Trait in a parameter or return position is shorthand for a trait bound and uses static dispatch (monomorphization). In return position it is especially useful when you want to return a closure or an iterator without exposing the concrete type — the compiler knows the exact type, the caller only sees the trait.

use std::fmt::Display;

// Parameter position — equivalent to fn notify<T: Display>(item: &T)
fn notify(item: &impl Display) {
    println!("Alert: {}", item);
}

// Return position — the concrete type is opaque; caller only sees Display
fn make_greeting(name: &str) -> impl Display {
    format!("Hello, {}!", name)
}

fn main() {
    notify(&42);
    notify(&"hello");
    println!("{}", make_greeting("Alice"));
}
$ rustc traits_impl_trait_syntax_static_dispatch.rs && ./traits_impl_trait_syntax_static_dispatch
Alert: 42
Alert: hello
Hello, Alice!

dyn Trait — Dynamic Dispatch

dyn Trait is a trait object — the concrete type is erased and method dispatch happens at runtime via a vtable (a table of function pointers). This costs a small indirection per call, but it enables heterogeneous collections where elements can be different types sharing a common interface. Use Box<dyn Trait> when you need to store or return values whose concrete type varies at runtime.

trait Animal {
    fn sound(&self) -> &str;
    fn name(&self) -> &str;
}

struct Dog;
struct Cat;

impl Animal for Dog {
    fn sound(&self) -> &str { "woof" }
    fn name(&self) -> &str { "dog" }
}

impl Animal for Cat {
    fn sound(&self) -> &str { "meow" }
    fn name(&self) -> &str { "cat" }
}

// Accepts a slice of any Animal — the types can differ
fn make_sounds(animals: &[Box<dyn Animal>]) {
    for animal in animals {
        println!("{} says {}", animal.name(), animal.sound());
    }
}

fn main() {
    // Dog and Cat can coexist in the same Vec thanks to dyn Trait
    let animals: Vec<Box<dyn Animal>> = vec![
        Box::new(Dog),
        Box::new(Cat),
        Box::new(Dog),
    ];
    make_sounds(&animals);
}
$ rustc traits_dyn_trait_dynamic_dispatch.rs && ./traits_dyn_trait_dynamic_dispatch
dog says woof
cat says meow
dog says woof

Implementing Standard Library Traits

Display and Debug

Implementing standard library traits integrates your types into the broader Rust ecosystem. Display controls what println!("{}", ...) shows to users. Debug controls println!("{:?}", ...) for developer output. Implementing them means your type works naturally with format strings, error messages, and logging.

use std::fmt;

struct Temperature {
    celsius: f64,
}

impl fmt::Display for Temperature {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        // User-facing: show both Celsius and the Fahrenheit conversion
        write!(f, "{:.1}°C ({:.1}°F)", self.celsius, self.celsius * 1.8 + 32.0)
    }
}

impl fmt::Debug for Temperature {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        // Developer-facing: show the raw struct fields
        write!(f, "Temperature {{ celsius: {} }}", self.celsius)
    }
}

fn main() {
    let t = Temperature { celsius: 100.0 };
    println!("{}", t);
    println!("{:?}", t);
}
$ rustc traits_display_and_debug.rs && ./traits_display_and_debug
100.0°C (212.0°F)
Temperature { celsius: 100 }

Look closely at the Debug line: it prints celsius: 100, not 100.0. The hand-written Debug impl formats the f64 with plain {}, which drops the .0 from whole numbers; #[derive(Debug)] would have printed 100.0, and the Display impl gets 100.0 only because it asks for {:.1}.

From / Into

From and Into are conversion traits that express “this type can be constructed from another type.” They matter especially for error handling — the ? operator uses From to automatically convert between error types. Implementing From<T> gives you Into<T> for free through a blanket impl in the standard library.

#[derive(Debug)]
struct Celsius(f64);
#[derive(Debug)]
struct Fahrenheit(f64);

impl From<Celsius> for Fahrenheit {
    fn from(c: Celsius) -> Fahrenheit {
        Fahrenheit(c.0 * 1.8 + 32.0)
    }
}

fn main() {
    let boiling = Celsius(100.0);
    let f: Fahrenheit = boiling.into(); // .into() uses the From impl above
    println!("{:?}", f);

    let freezing = Fahrenheit::from(Celsius(0.0));
    println!("{:?} -> {} degrees", freezing, freezing.0);
}
$ rustc traits_from_into.rs && ./traits_from_into
Fahrenheit(212.0)
Fahrenheit(32.0) -> 32 degrees

Only From<Celsius> for Fahrenheit was written, yet boiling.into() works: the standard library’s blanket impl<T, U: From<T>> Into<U> for T supplies it. The type annotation on f is what tells into() which target to pick.

Iterator

Implementing Iterator on your type unlocks the entire iterator adapter ecosystem for free — map, filter, take, zip, enumerate, sum, collect, and 70+ more methods all work automatically once you define a single next method. This is the power of trait-based design: you implement the minimal interface and inherit a huge amount of behavior.

struct Counter {
    count: u32,
    max: u32,
}

impl Counter {
    fn new(max: u32) -> Self { Self { count: 0, max } }
}

impl Iterator for Counter {
    type Item = u32;

    // The only method you must implement — everything else comes for free
    fn next(&mut self) -> Option<u32> {
        if self.count < self.max {
            self.count += 1;
            Some(self.count)
        } else {
            None
        }
    }
}

fn main() {
    // sum(), zip(), skip() all work automatically from the Iterator impl
    let sum: u32 = Counter::new(5).sum();
    println!("sum 1..=5: {}", sum);

    let pairs: Vec<_> = Counter::new(3)
        .zip(Counter::new(3).skip(1))
        .collect();
    println!("{:?}", pairs);
}
$ rustc traits_iterator.rs && ./traits_iterator
sum 1..=5: 15
[(1, 2), (2, 3)]

Operator Overloading via Traits

Rust implements operator overloading through traits in the std::ops module. Overloading + for a custom type means implementing the Add trait. This keeps the language consistent — operators are just syntax sugar for trait method calls, so you can always fall back to the method if needed.

use std::ops::{Add, Mul, Neg};

#[derive(Debug, Clone, Copy, PartialEq)]
struct Vec2 { x: f64, y: f64 }

impl Add for Vec2 {
    type Output = Vec2;
    fn add(self, rhs: Vec2) -> Vec2 {
        Vec2 { x: self.x + rhs.x, y: self.y + rhs.y }
    }
}

impl Mul<f64> for Vec2 {
    type Output = Vec2;
    fn mul(self, scalar: f64) -> Vec2 {
        Vec2 { x: self.x * scalar, y: self.y * scalar }
    }
}

impl Neg for Vec2 {
    type Output = Vec2;
    fn neg(self) -> Vec2 { Vec2 { x: -self.x, y: -self.y } }
}

fn main() {
    let a = Vec2 { x: 1.0, y: 2.0 };
    let b = Vec2 { x: 3.0, y: 4.0 };
    println!("{:?}", a + b);
    println!("{:?}", a * 3.0);
    println!("{:?}", -b);
    println!("{:?}", a.add(b)); // the method behind the + operator
}
$ rustc traits_operator_overloading_via_traits.rs && ./traits_operator_overloading_via_traits
Vec2 { x: 4.0, y: 6.0 }
Vec2 { x: 3.0, y: 6.0 }
Vec2 { x: -3.0, y: -4.0 }
Vec2 { x: 4.0, y: 6.0 }

a.add(b) needs use std::ops::Add in scope — which is already there — and prints exactly what a + b printed. 3.0 * a would not compile: only Vec2 * f64 was implemented, and f64 * Vec2 is a separate impl Mul<Vec2> for f64.

Supertraits

Sometimes a trait only makes sense when another trait is already implemented. Supertraits express this dependency — they say “to implement this trait, you must also implement these other traits.” This is useful for building layered abstractions where a higher-level capability requires lower-level capabilities to already be in place.

use std::fmt;

// Summary requires Display and Debug — any type implementing Summary
// must also implement both formatting traits
trait Summary: fmt::Display + fmt::Debug {
    fn summarize(&self) -> String;

    // Default implementation can use the supertrait methods
    fn print_summary(&self) {
        println!("Summary: {}", self.summarize());
        println!("Debug: {:?}", self);
    }
}

#[derive(Debug)]
struct Article {
    title: String,
    words: usize,
}

impl fmt::Display for Article {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "\"{}\"", self.title)
    }
}

impl Summary for Article {
    fn summarize(&self) -> String {
        format!("{} ({} words)", self, self.words)
    }
}

fn main() {
    let a = Article { title: String::from("Traits in Rust"), words: 1200 };
    a.print_summary();
}
$ rustc traits_supertraits.rs && ./traits_supertraits
Summary: "Traits in Rust" (1200 words)
Debug: Article { title: "Traits in Rust", words: 1200 }

print_summary is a default method on Summary, yet it can use {:?} on self — the supertrait bound guarantees every implementor is Debug. Remove #[derive(Debug)] from Article and the impl Summary for Article line fails to compile, even though Article never calls Debug itself.

Object Safety

A trait is object-safe (usable as dyn Trait) when the compiler can build a vtable for it. The vtable is a fixed-size table of function pointers, so every method must be dispatchable without knowing the concrete type at compile time. Methods that return Self break this because the return type size would vary; methods with generic parameters break it because there would need to be infinitely many vtable entries.

trait MyClone {
    fn my_clone(&self) -> Self;
}

struct Dot;

impl MyClone for Dot {
    fn my_clone(&self) -> Self { Dot }
}

fn main() {
    let items: Vec<Box<dyn MyClone>> = vec![Box::new(Dot)];
    println!("{}", items.len());
}
$ rustc traits_object_safety.rs
error[E0038]: the trait `MyClone` is not dyn compatible
  --> traits_object_safety.rs:12:28
   |
12 |     let items: Vec<Box<dyn MyClone>> = vec![Box::new(Dot)];
   |                            ^^^^^^^ `MyClone` is not dyn compatible
   |
note: for a trait to be dyn compatible it needs to allow building a vtable
      for more information, visit <https://doc.rust-lang.org/reference/items/traits.html#dyn-compatibility>
  --> traits_object_safety.rs:2:27
   |
 1 | trait MyClone {
   |       ------- this trait is not dyn compatible...
 2 |     fn my_clone(&self) -> Self;
   |                           ^^^^ ...because method `my_clone` references the `Self` type in its return type
   = help: consider moving `my_clone` to another trait
   = help: only type `Dot` implements `MyClone`; consider using it directly instead.

The compiler names the offending method and the reason. (Recent compilers say “dyn compatible” — the newer name for object safety.) A trait whose methods all take &self and return fixed-size types has no such problem:

trait Drawable {
    fn draw(&self);
    fn bounding_box(&self) -> (f64, f64, f64, f64);
}

struct Square(f64);
struct Dot;

impl Drawable for Square {
    fn draw(&self) { println!("square with side {}", self.0); }
    fn bounding_box(&self) -> (f64, f64, f64, f64) { (0.0, 0.0, self.0, self.0) }
}

impl Drawable for Dot {
    fn draw(&self) { println!("dot"); }
    fn bounding_box(&self) -> (f64, f64, f64, f64) { (0.0, 0.0, 0.0, 0.0) }
}

fn render(items: &[Box<dyn Drawable>]) {
    for item in items {
        item.draw();
        println!("  bbox: {:?}", item.bounding_box());
    }
}

fn main() {
    render(&[Box::new(Square(2.0)), Box::new(Dot)]);
}
$ rustc traits_object_safety_2.rs && ./traits_object_safety_2
square with side 2
  bbox: (0.0, 0.0, 2.0, 2.0)
dot
  bbox: (0.0, 0.0, 0.0, 0.0)

Practice

  1. Add a Robot { id: u32 } type that implements Greet with only hello (returning format!("BEEP. UNIT {} ONLINE.", self.id)). What does goodbye() return for it?

    Solution
    BEEP. UNIT 7 ONLINE.
    Goodbye!

    Robot didn’t write goodbye, so it inherits the trait’s default implementation — the same one English uses.

  2. In the operator-overloading example, write 3.0 * a instead of a * 3.0. Then make it compile.

    Solution
    error[E0277]: cannot multiply `{float}` by `Vec2`
    ...
       = help: the trait `Mul<Vec2>` is not implemented for `{float}`

    Operators dispatch on the left operand’s type. Add the mirror impl:

    impl Mul<Vec2> for f64 {
        type Output = Vec2;
        fn mul(self, v: Vec2) -> Vec2 {
            v * self
        }
    }

    3.0 * a then prints Vec2 { x: 3.0, y: 6.0 }.

  3. Remove #[derive(Debug)] from Article in the supertraits example. Which line does the compiler blame first?

    Solution
    error[E0277]: `Article` doesn't implement `Debug`
      --> ...
       |
       | impl Summary for Article {
       |                  ^^^^^^^ the trait `Debug` is not implemented for `Article`

    The error lands on the impl Summary line, not on any {:?}: the supertrait bound Summary: fmt::Debug is checked when you implement Summary.

  4. Implement From<Celsius> for Kelvin (Kelvin(c.0 + 273.15)) and convert Celsius(25.0) with .into(). What prints, and what extra output does rustc show?

    Solution

    It prints Kelvin(298.15), but rustc first warns field \0` is never readforKelvin: a derived Debugimpl doesn't count as reading a field. Printingk.0somewhere silences it — which is exactly why the lesson'sFrom/Intoexample printsfreezing.0`.

Frequently Asked Questions

What is a trait in Rust?
A trait defines a set of methods that a type must implement. It is similar to an interface in Java/Go or a typeclass in Haskell.
What is the difference between impl Trait and dyn Trait?
impl Trait is static dispatch — the concrete type is known at compile time and monomorphized. dyn Trait is dynamic dispatch — the concrete type is resolved at runtime via a vtable.
What is object safety?
A trait is object-safe if it can be used as dyn Trait. A trait is not object-safe if its methods return Self, use generic type parameters, or have other restrictions that prevent vtable dispatch.