ZetCode

Rust traits

last modified October 25, 2026

In this article we show how to define and use traits in Rust.

A trait defines a set of methods that a type must provide. It describes shared behaviour: different types can implement the same trait in their own way, and code written against the trait works with all of them. Traits are similar to interfaces in Java or C#.

Traits are central to Rust. Printing (Display, Debug), comparison (PartialEq, Ord), operators (Add), iteration (Iterator), and conversions (From) are all expressed as traits. Code that is generic over many types uses traits to say what it needs from those types.

The examples were tested with Rust 1.97 and the 2024 edition. They are run as Cargo projects with cargo run -q.

Defining and implementing a trait

A trait is defined with the trait keyword and implemented for a type with an impl Trait for Type block.

main.rs
trait Shape {
    fn area(&self) -> f64;
    fn name(&self) -> String;
}

struct Circle {
    radius: f64,
}

struct Rectangle {
    width: f64,
    height: f64,
}

impl Shape for Circle {
    fn area(&self) -> f64 {
        std::f64::consts::PI * self.radius * self.radius
    }

    fn name(&self) -> String {
        String::from("circle")
    }
}

impl Shape for Rectangle {
    fn area(&self) -> f64 {
        self.width * self.height
    }

    fn name(&self) -> String {
        String::from("rectangle")
    }
}

fn main() {
    let c = Circle { radius: 2.0 };
    let r = Rectangle { width: 3.0, height: 5.0 };

    println!("{} area: {:.2}", c.name(), c.area());
    println!("{} area: {:.2}", r.name(), r.area());
}

We define a Shape trait and implement it for two structs.

trait Shape {
    fn area(&self) -> f64;
    fn name(&self) -> String;
}

The trait lists method signatures ending with a semicolon. These are the required methods; every implementing type must provide them.

impl Shape for Circle {
    fn area(&self) -> f64 {
        std::f64::consts::PI * self.radius * self.radius
    }

    fn name(&self) -> String {
        String::from("circle")
    }
}

The implementation must define all required methods with matching signatures. Leaving one out is a compile-time error.

$ cargo run -q
circle area: 12.57
rectangle area: 15.00

Default methods

A trait can provide a default body for a method. Implementing types get the default for free and can override it.

main.rs
trait Greeter {
    fn name(&self) -> String;

    fn greet(&self) -> String {
        format!("Hello, {}!", self.name())
    }

    fn greet_loudly(&self) -> String {
        self.greet().to_uppercase()
    }
}

struct Person {
    first: String,
}

struct Robot {
    id: u32,
}

impl Greeter for Person {
    fn name(&self) -> String {
        self.first.clone()
    }
}

impl Greeter for Robot {
    fn name(&self) -> String {
        format!("unit {}", self.id)
    }

    fn greet(&self) -> String {
        format!("Greetings. I am {}.", self.name())
    }
}

fn main() {
    let p = Person { first: String::from("Lucia") };
    let r = Robot { id: 42 };

    println!("{}", p.greet());
    println!("{}", p.greet_loudly());
    println!("{}", r.greet());
    println!("{}", r.greet_loudly());
}

The Greeter trait has one required and two default methods.

fn greet(&self) -> String {
    format!("Hello, {}!", self.name())
}

A default method can call other methods of the trait, including the required ones whose bodies it does not know.

impl Greeter for Person {
    fn name(&self) -> String {
        self.first.clone()
    }
}

Person implements only the required name method and uses both defaults.

fn greet(&self) -> String {
    format!("Greetings. I am {}.", self.name())
}

Robot overrides greet. The default greet_loudly then calls the overridden version.

$ cargo run -q
Hello, Lucia!
HELLO, LUCIA!
Greetings. I am unit 42.
GREETINGS. I AM UNIT 42.

Trait bounds

Functions can accept any type that implements a given trait. The requirement is called a trait bound.

main.rs
use std::fmt::{Debug, Display};

trait Summary {
    fn summarize(&self) -> String;
}

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

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

fn notify(item: &impl Summary) {
    println!("new: {}", item.summarize());
}

fn notify_generic<T: Summary>(item: &T) {
    println!("generic: {}", item.summarize());
}

fn report<T, U>(item: &T, tag: U)
where
    T: Summary + Debug,
    U: Display,
{
    println!("[{tag}] {}", item.summarize());
    println!("{:?}", item);
}

fn main() {
    let a = Article { title: String::from("Rust traits"), words: 1800 };

    notify(&a);
    notify_generic(&a);
    report(&a, "draft");
    report(&a, 7);
}

The example shows three ways of writing trait bounds.

fn notify(item: &impl Summary) {

The impl Trait syntax in a parameter position is the shortest form. The function accepts a reference to any type that implements Summary.

fn notify_generic<T: Summary>(item: &T) {

The same function written with a generic type parameter. This form is needed when several parameters must have the same type, or when we want to refer to the type by name.

fn report<T, U>(item: &T, tag: U)
where
    T: Summary + Debug,
    U: Display,

Multiple bounds are combined with +. When the bounds get long, a where clause keeps the signature readable. The report function accepts any tag that implements Display, so both a string and an integer work.

$ cargo run -q
new: Rust traits (1800 words)
generic: Rust traits (1800 words)
[draft] Rust traits (1800 words)
Article { title: "Rust traits", words: 1800 }
[7] Rust traits (1800 words)
Article { title: "Rust traits", words: 1800 }

Trait objects

A trait object, written dyn Trait, lets us store values of different types that implement the same trait in one collection.

main.rs
trait Shape {
    fn area(&self) -> f64;
    fn name(&self) -> &str;
}

struct Circle(f64);
struct Square(f64);
struct Triangle(f64, f64);

impl Shape for Circle {
    fn area(&self) -> f64 { std::f64::consts::PI * self.0 * self.0 }
    fn name(&self) -> &str { "circle" }
}

impl Shape for Square {
    fn area(&self) -> f64 { self.0 * self.0 }
    fn name(&self) -> &str { "square" }
}

impl Shape for Triangle {
    fn area(&self) -> f64 { 0.5 * self.0 * self.1 }
    fn name(&self) -> &str { "triangle" }
}

fn total_area(shapes: &[Box<dyn Shape>]) -> f64 {
    shapes.iter().map(|s| s.area()).sum()
}

fn main() {
    let shapes: Vec<Box<dyn Shape>> = vec![
        Box::new(Circle(1.0)),
        Box::new(Square(2.0)),
        Box::new(Triangle(3.0, 4.0)),
        Box::new(Circle(0.5)),
    ];

    for s in &shapes {
        println!("{:<9} {:>6.2}", s.name(), s.area());
    }

    println!("total: {:.2}", total_area(&shapes));

    let largest = shapes
        .iter()
        .max_by(|a, b| a.area().total_cmp(&b.area()))
        .unwrap();
    println!("largest: {}", largest.name());
}

We store different shapes in a single vector.

let shapes: Vec<Box<dyn Shape>> = vec![
    Box::new(Circle(1.0)),
    Box::new(Square(2.0)),
    Box::new(Triangle(3.0, 4.0)),
    Box::new(Circle(0.5)),
];

The concrete types have different sizes, so they cannot be stored directly in a vector. Each value is put in a Box and the vector stores the boxes as Box<dyn Shape>. The concrete type is erased; only the trait methods are available.

fn total_area(shapes: &[Box<dyn Shape>]) -> f64 {
    shapes.iter().map(|s| s.area()).sum()
}

Calls on a trait object use dynamic dispatch: the method to call is looked up at runtime in a table of function pointers (vtable) stored next to the data pointer.

.max_by(|a, b| a.area().total_cmp(&b.area()))

Floating-point numbers do not implement Ord, so we compare the areas with total_cmp.

$ cargo run -q
circle      3.14
square      4.00
triangle    6.00
circle      0.79
total: 13.93
largest: triangle

Static and dynamic dispatch

Generic functions and trait objects both accept values of different types, but they work differently.

main.rs
trait Speak {
    fn speak(&self) -> String;
}

struct Dog;
struct Cat;

impl Speak for Dog {
    fn speak(&self) -> String { String::from("woof") }
}

impl Speak for Cat {
    fn speak(&self) -> String { String::from("meow") }
}

fn static_call<T: Speak>(animal: &T) -> String {
    animal.speak()
}

fn dynamic_call(animal: &dyn Speak) -> String {
    animal.speak()
}

fn main() {
    println!("{}", static_call(&Dog));
    println!("{}", static_call(&Cat));

    let animals: [&dyn Speak; 3] = [&Dog, &Cat, &Dog];
    let sounds: Vec<String> = animals.iter().map(|a| dynamic_call(*a)).collect();
    println!("{}", sounds.join(" "));

    println!("&Dog: {} bytes", std::mem::size_of::<&Dog>());
    println!("&dyn Speak: {} bytes", std::mem::size_of::<&dyn Speak>());
}

We call the same trait method through a generic function and through a trait object.

fn static_call<T: Speak>(animal: &T) -> String {
    animal.speak()
}

For a generic function, the compiler creates a separate copy for each concrete type it is used with, here static_call::<Dog> and static_call::<Cat>. The calls are resolved at compile time and can be inlined. This is static dispatch.

fn dynamic_call(animal: &dyn Speak) -> String {
    animal.speak()
}

There is only one copy of dynamic_call; the method is found through the vtable at runtime. In exchange for a small runtime cost, we can mix types in one array and the binary does not grow with each new type.

println!("&dyn Speak: {} bytes", std::mem::size_of::<&dyn Speak>());

A reference to a trait object is a fat pointer: it holds a pointer to the data and a pointer to the vtable, so it is twice the size of a normal reference on a 64-bit system.

$ cargo run -q
woof
meow
woof meow woof
&Dog: 8 bytes
&dyn Speak: 16 bytes

Returning traits

A function can return a type that implements a trait without naming the type.

main.rs
use std::fmt::Display;

fn evens(limit: u32) -> impl Iterator<Item = u32> {
    (0..limit).filter(|n| n % 2 == 0)
}

fn make_adder(n: i32) -> impl Fn(i32) -> i32 {
    move |x| x + n
}

fn label(flag: bool) -> Box<dyn Display> {
    if flag {
        Box::new("enabled")
    } else {
        Box::new(0)
    }
}

fn main() {
    let v: Vec<u32> = evens(12).collect();
    println!("{:?}", v);

    let add5 = make_adder(5);
    println!("{}", add5(10));

    println!("{}", label(true));
    println!("{}", label(false));
}

The example returns an iterator, a closure, and a trait object.

fn evens(limit: u32) -> impl Iterator<Item = u32> {
    (0..limit).filter(|n| n % 2 == 0)
}

The real return type is a Filter struct with a closure type inside it, which cannot even be written down. impl Iterator hides it; the caller only knows it gets an iterator over u32.

fn make_adder(n: i32) -> impl Fn(i32) -> i32 {
    move |x| x + n
}

Every closure has its own unique type, so returning closures requires impl Fn.

fn label(flag: bool) -> Box<dyn Display> {
    if flag {
        Box::new("enabled")
    } else {
        Box::new(0)
    }
}

impl Trait stands for one concrete type chosen by the function. When different branches return different types, a &str and an i32 here, we must return a trait object instead.

$ cargo run -q
[0, 2, 4, 6, 8, 10]
15
enabled
0

Implementing standard traits

Implementing traits from the standard library integrates our types with the language: formatting, operators, and conversions.

main.rs
use std::fmt;
use std::ops::Add;

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

impl fmt::Display for Vec2 {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "[{}, {}]", self.x, self.y)
    }
}

impl Add for Vec2 {
    type Output = Vec2;

    fn add(self, other: Vec2) -> Vec2 {
        Vec2 { x: self.x + other.x, y: self.y + other.y }
    }
}

impl From<(f64, f64)> for Vec2 {
    fn from((x, y): (f64, f64)) -> Self {
        Vec2 { x, y }
    }
}

fn main() {
    let a = Vec2 { x: 1.5, y: 2.0 };
    let b: Vec2 = (0.5, -4.0).into();
    let c = Vec2::from((3.0, 3.0));

    println!("a = {a}");
    println!("a + b = {}", a + b);
    println!("a + b + c = {}", a + b + c);
    println!("{}", a + b == Vec2 { x: 2.0, y: -2.0 });
}

The Vec2 struct implements Display, Add, and From.

impl Add for Vec2 {
    type Output = Vec2;

    fn add(self, other: Vec2) -> Vec2 {
        Vec2 { x: self.x + other.x, y: self.y + other.y }
    }
}

Operators in Rust are traits from std::ops. Implementing Add enables the + operator. The Output associated type defines the type of the result.

impl From<(f64, f64)> for Vec2 {
    fn from((x, y): (f64, f64)) -> Self {
        Vec2 { x, y }
    }
}

Implementing From gives us Vec2::from, and the standard library automatically provides the matching Into implementation, so we can also write (0.5, -4.0).into().

$ cargo run -q
a = [1.5, 2]
a + b = [2, -2]
a + b + c = [5, 1]
true

Associated types and constants

Besides methods, a trait can declare types and constants that each implementation defines.

main.rs
struct Countdown {
    current: u32,
}

impl Iterator for Countdown {
    type Item = u32;

    fn next(&mut self) -> Option<Self::Item> {
        if self.current == 0 {
            None
        } else {
            self.current -= 1;
            Some(self.current + 1)
        }
    }
}

trait Unit {
    const SYMBOL: &'static str;
    const FACTOR: f64;

    fn to_base(value: f64) -> f64 {
        value * Self::FACTOR
    }
}

struct Kilometer;
struct Mile;

impl Unit for Kilometer {
    const SYMBOL: &'static str = "km";
    const FACTOR: f64 = 1000.0;
}

impl Unit for Mile {
    const SYMBOL: &'static str = "mi";
    const FACTOR: f64 = 1609.344;
}

fn main() {
    let cd = Countdown { current: 5 };
    let v: Vec<u32> = cd.collect();
    println!("{:?}", v);

    let sum: u32 = Countdown { current: 10 }.filter(|n| n % 3 == 0).sum();
    println!("sum: {sum}");

    println!("3 {} = {} m", Kilometer::SYMBOL, Kilometer::to_base(3.0));
    println!("3 {} = {} m", Mile::SYMBOL, Mile::to_base(3.0));
}

We implement the Iterator trait and define our own trait with associated constants.

impl Iterator for Countdown {
    type Item = u32;

    fn next(&mut self) -> Option<Self::Item> {

The Iterator trait has an associated type Item and one required method, next. Once next is implemented, all the other iterator methods such as filter, sum, and collect come as default methods.

trait Unit {
    const SYMBOL: &'static str;
    const FACTOR: f64;

    fn to_base(value: f64) -> f64 {
        value * Self::FACTOR
    }
}

Associated constants are declared in the trait and given values in each implementation. The to_base function has no self parameter; it is called on the type as Mile::to_base(3.0).

$ cargo run -q
[5, 4, 3, 2, 1]
sum: 18
3 km = 3000 m
3 mi = 4828.032 m

Supertraits

A trait can require that implementing types also implement other traits. These are called supertraits.

main.rs
use std::fmt::Display;

trait Named {
    fn name(&self) -> String;
}

trait Employee: Named + Display {
    fn salary(&self) -> u32;

    fn card(&self) -> String {
        format!("{} | {} | {}", self.name(), self, self.salary())
    }
}

struct Developer {
    name: String,
    lang: String,
}

impl Named for Developer {
    fn name(&self) -> String {
        self.name.clone()
    }
}

impl Display for Developer {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        write!(f, "{} developer", self.lang)
    }
}

impl Employee for Developer {
    fn salary(&self) -> u32 {
        4500
    }
}

fn main() {
    let d = Developer { name: String::from("Tomas"), lang: String::from("Rust") };
    println!("{}", d.card());
}

The Employee trait builds on Named and Display.

trait Employee: Named + Display {
    fn salary(&self) -> u32;

    fn card(&self) -> String {
        format!("{} | {} | {}", self.name(), self, self.salary())
    }
}

Because every Employee is guaranteed to be Named and Display, the default card method can call name and format self with {}. A type that implements Employee without the supertraits does not compile.

$ cargo run -q
Tomas | Rust developer | 4500

Extending existing types

We can implement our own traits for types we did not define, including types from the standard library.

main.rs
trait StrExt {
    fn word_count(&self) -> usize;
    fn is_palindrome(&self) -> bool;
}

impl StrExt for str {
    fn word_count(&self) -> usize {
        self.split_whitespace().count()
    }

    fn is_palindrome(&self) -> bool {
        let chars: Vec<char> = self
            .chars()
            .filter(|c| c.is_alphanumeric())
            .map(|c| c.to_ascii_lowercase())
            .collect();
        chars.iter().eq(chars.iter().rev())
    }
}

trait Double {
    fn double(&self) -> Self;
}

impl Double for i32 {
    fn double(&self) -> Self {
        self * 2
    }
}

fn main() {
    let s = "an old falcon in the sky";
    println!("words: {}", s.word_count());

    for w in ["level", "Never odd or even", "falcon"] {
        println!("{w}: {}", w.is_palindrome());
    }

    println!("{}", 21.double());
}

We add methods to str and i32.

impl StrExt for str {

After the implementation, every string slice has the word_count and is_palindrome methods, as long as the StrExt trait is in scope.

chars.iter().eq(chars.iter().rev())

A palindrome reads the same forwards and backwards; we compare the iterator with its reversed version.

The orphan rule limits this: an implementation is allowed only if the trait or the type is defined in our crate. We can implement our StrExt for str, or Display for our own struct, but not Display for Vec<i32>.

$ cargo run -q
words: 6
level: true
Never odd or even: true
falcon: false
42

Blanket implementations

A blanket implementation implements a trait for all types that satisfy a bound.

main.rs
use std::fmt::Display;

trait Describe {
    fn describe(&self) -> String;
}

impl<T: Display> Describe for T {
    fn describe(&self) -> String {
        let s = self.to_string();
        format!("'{}' ({} chars)", s, s.chars().count())
    }
}

fn main() {
    println!("{}", 42.describe());
    println!("{}", 2.75.describe());
    println!("{}", "hello".describe());
    println!("{}", 'x'.describe());
    println!("{}", String::from("žltý kôň").describe());
}

We implement Describe for every type that implements Display.

impl<T: Display> Describe for T {

Integers, floats, strings, and characters all get the describe method with a single implementation. The standard library uses the same technique: to_string is available on every Display type through a blanket implementation of the ToString trait.

format!("'{}' ({} chars)", s, s.chars().count())

We count characters with chars().count(), since len returns the number of bytes; the accented letters of the last string take two bytes each in UTF-8.

$ cargo run -q
'42' (2 chars)
'2.75' (4 chars)
'hello' (5 chars)
'x' (1 chars)
'žltý kôň' (8 chars)

Source

The Rust Programming Language: Traits

The Rust Programming Language: Using Trait Objects

In this article we have worked with traits in Rust. Their use with type parameters is covered in Rust generics.

Author

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all Rust tutorials.