Lesson 14 of 22ยท17 min

Traits

Different types often support the same operation. A circle and a square both have an area; a u32, a String and your own struct can all be printed; integers and strings can both be compared with ==. A trait names a behaviour like that: a set of methods a type promises to provide. Any type can sign up by implementing the trait, and code can then work with "anything that has an area" instead of one specific type.

You have used traits since the first lesson without writing one: Debug is what {:?} needs, PartialEq is what == needs, and From is what ? uses to convert errors. This lesson shows how to define your own traits, implement them, give them default methods, implement and derive the standard ones, and use Default to build values with sensible starting fields.

Defining and implementing a trait

A trait lists method signatures, ending each with a ; instead of a body. impl TraitName for TypeName then provides those methods for one type, with signatures that match the trait exactly:

trait Shape {
    fn area(&self) -> f64;
    fn perimeter(&self) -> f64;
}
 
struct Circle {
    radius: f64,
}
 
struct Square {
    side: f64,
}
 
impl Shape for Circle {
    fn area(&self) -> f64 {
        std::f64::consts::PI * self.radius * self.radius
    }
 
    fn perimeter(&self) -> f64 {
        2.0 * std::f64::consts::PI * self.radius
    }
}
 
impl Shape for Square {
    fn area(&self) -> f64 {
        self.side * self.side
    }
 
    fn perimeter(&self) -> f64 {
        4.0 * self.side
    }
}
 
fn main() {
    let wheel = Circle { radius: 1.5 };
    let tile = Square { side: 2.0 };
    println!("{:.2} {:.2}", wheel.area(), wheel.perimeter());
    println!("{:.2} {:.2}", tile.area(), tile.perimeter());
}
7.07 9.42
4.00 8.00

The trait says: a type that is a Shape has an area method and a perimeter method, both taking &self and returning an f64. It doesn't say how they are computed; each impl block decides that for its own type. Trait names are UpperCamelCase, like types, and often describe a capability: Clone, Display, Iterator, Hash.

Trait methods are called like any other method. A type can implement as many traits as it likes, and it can still have its own impl Circle { ... } block for methods that aren't part of a trait.

Every method the trait declares must be implemented, and you can't add extra methods to an impl Trait for Type block. Those go in a plain impl block.

Default methods

A trait can give a method a body. That body is the default: every type that implements the trait gets it for free, and a type can override it with its own version. Default methods can call the other methods of the trait, even the ones without a body:

trait Shape {
    fn area(&self) -> f64;
 
    fn name(&self) -> String {
        String::from("shape")
    }
 
    fn summary(&self) -> String {
        format!("{} with area {:.1}", self.name(), self.area())
    }
}
 
struct Triangle {
    base: f64,
    height: f64,
}
 
struct Hexagon {
    side: f64,
}
 
impl Shape for Triangle {
    fn area(&self) -> f64 {
        self.base * self.height / 2.0
    }
 
    fn name(&self) -> String {
        String::from("triangle")
    }
}
 
impl Shape for Hexagon {
    fn area(&self) -> f64 {
        3.0 * 3.0_f64.sqrt() / 2.0 * self.side * self.side
    }
}
 
fn main() {
    let sail = Triangle {
        base: 3.0,
        height: 4.0,
    };
    let nut = Hexagon { side: 2.0 };
    println!("{}", sail.summary());
    println!("{}", nut.summary());
}
triangle with area 6.0
shape with area 10.4

Triangle overrides name, Hexagon keeps the default, and both get summary without writing it. This is how the standard library's Iterator trait works: you implement one method, next, and get dozens of others (map, filter, sum, ...) as default methods built on it.

Traits as parameters

The point of a shared trait is writing code that works for every type that implements it. impl Shape as a parameter type means "any type that implements Shape":

trait Shape {
    fn area(&self) -> f64;
}
 
struct Circle {
    radius: f64,
}
 
struct Square {
    side: f64,
}
 
impl Shape for Circle {
    fn area(&self) -> f64 {
        std::f64::consts::PI * self.radius * self.radius
    }
}
 
impl Shape for Square {
    fn area(&self) -> f64 {
        self.side * self.side
    }
}
 
fn paint_needed(shape: &impl Shape, coats: u32) -> f64 {
    shape.area() * coats as f64 * 0.1
}
 
fn main() {
    let table = Circle { radius: 0.6 };
    let wall = Square { side: 3.0 };
    println!("{:.3} litres", paint_needed(&table, 2));
    println!("{:.3} litres", paint_needed(&wall, 3));
}
0.226 litres
2.700 litres

Inside paint_needed, only the methods of Shape are available on shape, because that's all the function knows about it. impl Trait is shorthand for a generic function with a trait bound; the next lesson on generics covers that in full.

Traits and modules

A trait's methods can only be called where the trait is in scope. If a trait is defined in another module, bring it in with use, even though you only call its methods and never name it:

mod units {
    pub trait Metric {
        fn to_metres(&self) -> f64;
    }
 
    impl Metric for f64 {
        fn to_metres(&self) -> f64 {
            self * 0.3048
        }
    }
}
 
use units::Metric;
 
fn main() {
    let feet: f64 = 10.0;
    println!("{:.3} m", feet.to_metres());
}
3.048 m

This example also shows that you can implement your own trait on a type you didn't write, here the built-in f64.

Implementing standard traits

The standard library defines traits that plug your types into the language. Implement std::fmt::Display and your type works with {} in println! and format!, and gets a to_string() method as well:

use std::fmt;
 
struct Temperature {
    celsius: f64,
}
 
impl fmt::Display for Temperature {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "{:.1}ยฐC", self.celsius)
    }
}
 
fn main() {
    let outside = Temperature { celsius: -3.4 };
    println!("It is {outside} outside");
    let label: String = outside.to_string();
    println!("{} characters", label.chars().count());
}
It is -3.4ยฐC outside
6 characters

fmt receives a formatter f and writes into it with write!, returning the fmt::Result that write! gives back.

Some other standard traits you'll implement by hand:

TraitGives you
fmt::Display{} formatting and .to_string()
From<T>Type::from(t), t.into(), and ? conversions
std::ops::Add etc.Operators like + (see the advanced traits lesson)
Iteratorfor loops and every iterator method
DropCode that runs when a value goes out of scope

Deriving traits

For many standard traits, the implementation is obvious from the fields: two structs are equal if all their fields are equal, and cloning a struct clones each field. For those, #[derive(...)] writes the impl for you:

#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
struct Version {
    major: u32,
    minor: u32,
    patch: u32,
}
 
fn main() {
    let installed = Version {
        major: 1,
        minor: 9,
        patch: 4,
    };
    let latest = Version {
        major: 1,
        minor: 10,
        patch: 0,
    };
 
    let copy = installed.clone();
    println!("{}", copy == installed);
    println!("{}", installed < latest);
    println!("{:?}", installed.clone().max(latest));
}
true
true
Version { major: 1, minor: 10, patch: 0 }

The derived comparisons look at the fields in the order they are declared, so major is compared first, then minor, then patch. That's why 1.9.4 is less than 1.10.0 here, and why field order matters when you derive PartialOrd.

The traits you can derive from the standard library are:

TraitNeeded for
Debug{:?}, dbg!, assert_eq! failure messages
Clone.clone()
CopyCopying on assignment instead of moving (needs Clone)
PartialEq, Eq== and !=
PartialOrd, Ord<, >, max, sorting
HashUse as a HashMap key or in a HashSet (with Eq)
DefaultType::default()

Deriving only works if every field implements the trait too: a struct can only derive Clone if all its fields are Clone. Copy has a stricter rule: the fields must all be Copy, so a struct holding a String can never be Copy.

Eq and Ord are the stricter versions of PartialEq and PartialOrd. They promise that every value equals itself and every pair of values can be ordered. Floating-point numbers break that (NaN != NaN), so a struct with an f64 field can derive PartialEq and PartialOrd but not Eq, Ord or Hash.

The Default trait

Default gives a type a starting value, through one function: Default::default(). The built-in types already have one: 0 for numbers, false for bool, an empty String or Vec, and None for an Option.

fn main() {
    let count: u32 = Default::default();
    let name = String::default();
    let tags: Vec<&str> = Vec::default();
    let nickname: Option<String> = Default::default();
    println!("{count} {name:?} {tags:?} {nickname:?}");
}
0 "" [] None

#[derive(Default)] builds a value from the defaults of each field:

#[derive(Default)]
struct Stats {
    games: u32,
    wins: u32,
    best_streak: u32,
    favourite_map: Option<String>,
}
 
fn main() {
    let fresh = Stats::default();
    println!(
        "{} games, {} wins, best streak {}",
        fresh.games, fresh.wins, fresh.best_streak
    );
    println!("{:?}", fresh.favourite_map);
}
0 games, 0 wins, best streak 0
None

When the zero values aren't the right starting point, implement Default yourself. It has a single function, default, that returns Self:

#[derive(Debug)]
struct WindowOptions {
    title: String,
    width: u32,
    height: u32,
    resizable: bool,
}
 
impl Default for WindowOptions {
    fn default() -> Self {
        WindowOptions {
            title: String::from("Untitled"),
            width: 800,
            height: 600,
            resizable: true,
        }
    }
}
 
fn main() {
    let window = WindowOptions::default();
    println!("{window:?}");
 
    let dialog = WindowOptions {
        title: String::from("Save as"),
        resizable: false,
        ..Default::default()
    };
    println!(
        "{} {}x{} {}",
        dialog.title, dialog.width, dialog.height, dialog.resizable
    );
}
WindowOptions { title: "Untitled", width: 800, height: 600, resizable: true }
Save as 800x600 false

The second value uses the struct update syntax from the structs lesson with ..Default::default(): set the fields you care about and take the rest from the default. Rust knows which type's default to call from the struct name. This is the usual way to build a value with many optional settings.

Default also powers methods you've already seen, like unwrap_or_default() on an Option and or_default() on a HashMap entry: they call T::default() when there's no value.

The orphan rule

You can implement a trait for a type as long as either the trait or the type is defined in your crate. Your trait on f64 is fine (as in the Metric example), and so is Display on your Temperature. What you can't do is implement a standard trait on a standard type, like Display for Vec<i32>. This is the orphan rule: it guarantees two crates can never provide conflicting implementations of the same trait for the same type. If you need that, wrap the type in a struct of your own (a newtype) and implement the trait on the wrapper.

Common mistakes

Forgetting a method in the impl

trait Shape {
    fn area(&self) -> f64;
    fn perimeter(&self) -> f64;
}
 
struct Square {
    side: f64,
}
 
impl Shape for Square {
    fn area(&self) -> f64 {
        self.side * self.side
    }
}
 
fn main() {
    println!("{}", Square { side: 2.0 }.area());
}

Every method without a default body has to be implemented. Add the missing one, or, if most types would share the same body, give it a default in the trait.

Calling a trait method without the trait in scope

mod units {
    pub trait Metric {
        fn to_metres(&self) -> f64;
    }
 
    impl Metric for f64 {
        fn to_metres(&self) -> f64 {
            self * 0.3048
        }
    }
}
 
fn main() {
    let feet: f64 = 10.0;
    println!("{}", feet.to_metres());
}

The method exists, but only where the trait is visible. The compiler even names the trait to import: add use units::Metric;.

Deriving Copy on a type that can't be copied

#[derive(Clone, Copy)]
struct Player {
    name: String,
    score: u32,
}
 
fn main() {
    let one = Player {
        name: String::from("ferris"),
        score: 10,
    };
    let two = one;
    println!("{} {}", two.name, two.score);
}

A String owns heap memory, so copying it bit by bit would give two owners of the same buffer. Drop Copy and call .clone() when you need a second value, or keep Copy only on types made entirely of Copy fields.

Implementing a foreign trait on a foreign type

use std::fmt;
 
impl fmt::Display for Vec<u32> {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "{} items", self.len())
    }
}
 
fn main() {
    println!("{}", vec![1, 2, 3]);
}

Both Display and Vec come from the standard library, so the orphan rule forbids it. Wrap the vector in your own type, such as struct Basket(Vec<u32>);, and implement Display for Basket.

Summary

  • A trait is a named set of methods. impl Trait for Type implements it, and every method without a default must be provided with a matching signature.
  • Default methods have a body in the trait. Implementors get them for free and can override them; they can call the trait's other methods.
  • &impl Trait as a parameter accepts any type that implements the trait.
  • A trait's methods are only callable where the trait is in scope; use it.
  • Implement fmt::Display for {} and to_string(). Derive Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash and Default when the field-by-field behaviour is what you want.
  • Default::default() gives a starting value. Derive it for zero values, implement it for your own, and combine it with ..Default::default() to set only some fields.
  • The orphan rule: the trait or the type must be yours.

Practise what you read

Challenges that use the ideas from this lesson.