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.00The 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.4Triangle 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 litresInside 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 mThis 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 charactersfmt 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:
| Trait | Gives 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) |
Iterator | for loops and every iterator method |
Drop | Code 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:
| Trait | Needed for |
|---|---|
Debug | {:?}, dbg!, assert_eq! failure messages |
Clone | .clone() |
Copy | Copying on assignment instead of moving (needs Clone) |
PartialEq, Eq | == and != |
PartialOrd, Ord | <, >, max, sorting |
Hash | Use as a HashMap key or in a HashSet (with Eq) |
Default | Type::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
NoneWhen 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 falseThe 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 Typeimplements 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 Traitas a parameter accepts any type that implements the trait.- A trait's methods are only callable where the trait is in scope;
useit. - Implement
fmt::Displayfor{}andto_string(). DeriveDebug,Clone,Copy,PartialEq,Eq,PartialOrd,Ord,HashandDefaultwhen 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.