Advanced Traits
The traits lesson covered the everyday parts of traits: defining them,
implementing them, default methods and derives. The generics and trait
objects lessons used them as bounds and as dyn Trait. This lesson covers the
features you meet as soon as you read the standard library's own traits:
associated types (the Item in Iterator<Item = u32>), supertraits
(a trait that requires another one), operator overloading (what +
actually calls), default type parameters, and fully qualified syntax
for when two traits give a type methods with the same name.
Associated types
Here is a simplified version of the Iterator trait from the standard
library:
trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
}type Item; declares an associated type: a placeholder type that every
implementation fills in. Inside the trait it is written Self::Item, "the
Item type of whichever type implements this". An iterator over u32s sets
it to u32, an iterator over lines sets it to String, and next returns
Option<u32> or Option<String> accordingly.
Implementing Iterator for your own type means picking the Item and writing
next:
struct Countdown {
remaining: u32,
}
impl Iterator for Countdown {
type Item = u32;
fn next(&mut self) -> Option<Self::Item> {
if self.remaining == 0 {
return None;
}
let current = self.remaining;
self.remaining -= 1;
Some(current)
}
}
fn main() {
for n in (Countdown { remaining: 3 }) {
println!("{n}...");
}
println!("liftoff");
let total: u32 = Countdown { remaining: 4 }.sum();
let all: Vec<u32> = Countdown { remaining: 5 }.collect();
println!("{total} {all:?}");
}3...
2...
1...
liftoff
10 [5, 4, 3, 2, 1]Writing next is the only work. for loops, sum, collect and dozens of
other methods are default methods of Iterator, and they all work with
Self::Item, so they come for free. The closures and iterators lesson goes
through those methods.
Your own trait with an associated type
Associated types are useful whenever a trait's methods deal with a type that depends on the implementation. A sensor returns a reading, but a thermometer's reading is a temperature and a door switch's reading is open or closed:
use std::fmt::Debug;
trait Sensor {
type Reading;
fn name(&self) -> String;
fn read(&mut self) -> Self::Reading;
}
struct Thermometer {
celsius: f64,
}
struct DoorSwitch {
open: bool,
}
impl Sensor for Thermometer {
type Reading = f64;
fn name(&self) -> String {
String::from("thermometer")
}
fn read(&mut self) -> f64 {
self.celsius += 0.5;
self.celsius
}
}
impl Sensor for DoorSwitch {
type Reading = bool;
fn name(&self) -> String {
String::from("front door")
}
fn read(&mut self) -> bool {
self.open = !self.open;
self.open
}
}
fn poll<S: Sensor>(sensor: &mut S) -> S::Reading
where
S::Reading: Debug,
{
let reading = sensor.read();
println!("{}: {reading:?}", sensor.name());
reading
}
fn main() {
let mut thermometer = Thermometer { celsius: 20.0 };
let mut door = DoorSwitch { open: false };
let temperature = poll(&mut thermometer);
poll(&mut door);
poll(&mut door);
println!("above 20: {}", temperature > 20.0);
}thermometer: 20.5
front door: true
front door: false
above 20: trueTwo details are worth noticing. In an impl, read can return f64
directly instead of Self::Reading, since they are the same type there. And
in the generic poll, S::Reading names "the reading type of S", so the
function can return it and put a bound on it (S::Reading: Debug) without
knowing what it is. Because poll returns S::Reading, temperature is an
f64 and can be compared with 20.0.
Constraining an associated type
In a bound, Trait<Name = Type> requires the associated type to be one
specific type. This is how you write "any iterator of f64s":
fn average(values: impl Iterator<Item = f64>) -> f64 {
let mut sum = 0.0;
let mut count = 0;
for value in values {
sum += value;
count += 1;
}
sum / count as f64
}
fn main() {
let readings = vec![19.5, 21.0, 22.5];
println!("{}", average(readings.into_iter()));
println!("{}", average([1.0, 2.0].into_iter()));
}21
1.5Associated types or generic parameters?
A trait could take the type as a generic parameter instead, as in
trait Sensor<R>. The difference is how many implementations one type can
have:
- With a generic parameter, a type can implement the trait many times, once
per
R.From<T>works like this:StringimplementsFrom<&str>,From<char>,From<Box<str>>and more. - With an associated type, a type implements the trait once and the
associated type is fixed by that implementation.
Countdownyieldsu32s and nothing else, soIteratoruses an associated type.
The associated type also keeps signatures short: you write S: Sensor, not
S: Sensor<R> plus an extra R parameter on every function that uses it.
A good rule: if there should be exactly one answer per implementing type, use
an associated type.
Supertraits
Sometimes a trait only makes sense for types that also implement another
trait. A trait for alerts that prints itself needs the alert to be
displayable. Writing trait Alert: fmt::Display makes Display a
supertrait of Alert: every type that implements Alert must implement
Display too, and in return Alert's methods can use Display:
use std::fmt;
trait Alert: fmt::Display {
fn level(&self) -> u8;
fn announce(&self) {
let marker = "!".repeat(self.level() as usize);
println!("{marker} {self}");
}
}
struct DiskFull {
percent: u8,
}
struct HostDown {
host: String,
}
impl fmt::Display for DiskFull {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "disk is {}% full", self.percent)
}
}
impl Alert for DiskFull {
fn level(&self) -> u8 {
if self.percent > 95 { 3 } else { 1 }
}
}
impl fmt::Display for HostDown {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{} is not responding", self.host)
}
}
impl Alert for HostDown {
fn level(&self) -> u8 {
2
}
}
fn worst(alerts: &[&dyn Alert]) -> String {
let mut worst = alerts[0];
for alert in alerts {
if alert.level() > worst.level() {
worst = *alert;
}
}
worst.to_string()
}
fn main() {
let disk = DiskFull { percent: 97 };
let host = HostDown {
host: String::from("db-2"),
};
disk.announce();
host.announce();
println!("worst: {}", worst(&[&host, &disk]));
}!!! disk is 97% full
!! db-2 is not responding
worst: disk is 97% fullThe default method announce formats self with {self}, which needs
Display. That compiles only because of the supertrait: the compiler knows
that whatever type implements Alert is also Display. The same goes for
worst: it only asks for &dyn Alert, yet it can call to_string(), which
comes from Display.
A supertrait is a requirement, not inheritance. DiskFull implements
Display and Alert in two separate impl blocks, and Alert doesn't
inherit or override anything from Display. A trait can have several
supertraits, written with +: trait Record: fmt::Debug + Clone + PartialEq.
You've already seen supertraits in the standard library: Copy requires
Clone, Eq requires PartialEq, and Ord requires Eq and PartialOrd.
That is why the derivable traits table in the traits lesson says Copy needs
Clone.
Operator overloading
Operators in Rust are calls to trait methods. a + b is
std::ops::Add::add(a, b), a == b is PartialEq::eq(&a, &b), and so on.
Implement the trait and the operator works for your type. Here is the Add
trait:
trait Add<Rhs = Self> {
type Output;
fn add(self, rhs: Rhs) -> Self::Output;
}It has a generic parameter Rhs, the type on the right of the +, and an
associated type Output, the type of the result. A 2D vector type can add
two vectors, subtract them and negate one:
use std::ops::{Add, Neg, Sub};
#[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 Sub for Vec2 {
type Output = Vec2;
fn sub(self, rhs: Vec2) -> Vec2 {
Vec2 {
x: self.x - rhs.x,
y: self.y - rhs.y,
}
}
}
impl Neg for Vec2 {
type Output = Vec2;
fn neg(self) -> Vec2 {
Vec2 {
x: -self.x,
y: -self.y,
}
}
}
fn main() {
let position = Vec2 { x: 1.0, y: 2.0 };
let velocity = Vec2 { x: 0.5, y: -1.0 };
let next = position + velocity;
println!("{next:?}");
println!("{:?}", next - position);
println!("{:?}", -velocity);
println!("{}", next - velocity == position);
}Vec2 { x: 1.5, y: 1.0 }
Vec2 { x: 0.5, y: -1.0 }
Vec2 { x: -0.5, y: 1.0 }
trueimpl Add for Vec2 doesn't mention Rhs, so it gets the default, Self:
this implementation adds a Vec2 to a Vec2. == works because of the
derived PartialEq, and position can be used again after
position + velocity because Vec2 is Copy. More on that in the mistakes below.
Default type parameters
Rhs = Self in trait Add<Rhs = Self> is a default type parameter: if an
implementation doesn't say what Rhs is, it is Self. You can override it to
put a different type on the right of the operator. Scaling a vector by a
number is Vec2 * f64, so it implements Mul<f64>:
use std::ops::{AddAssign, Mul};
#[derive(Debug, Clone, Copy)]
struct Vec2 {
x: f64,
y: f64,
}
impl Mul<f64> for Vec2 {
type Output = Vec2;
fn mul(self, factor: f64) -> Vec2 {
Vec2 {
x: self.x * factor,
y: self.y * factor,
}
}
}
impl AddAssign for Vec2 {
fn add_assign(&mut self, rhs: Vec2) {
self.x += rhs.x;
self.y += rhs.y;
}
}
fn main() {
let velocity = Vec2 { x: 2.0, y: 0.5 };
let mut position = Vec2 { x: 0.0, y: 0.0 };
for _ in 0..3 {
position += velocity * 0.5;
}
println!("{position:?}");
}Vec2 { x: 3.0, y: 0.75 }Mul<f64> for Vec2 covers vec * 2.0 only. The operands are not swapped for
you: 2.0 * vec would need impl Mul<Vec2> for f64. Each combination of
left type, right type and operator is its own implementation, and Output
doesn't have to match either side.
AddAssign is the trait behind +=. Its method takes &mut self and returns
nothing, because it changes the left side in place instead of producing a new
value.
Which operators you can overload
Every overloadable operator has a trait in std::ops, or in std::cmp for
comparisons:
| Operator | Trait | Method |
|---|---|---|
a + b | Add | add(self, rhs) |
a - b | Sub | sub(self, rhs) |
a * b | Mul | mul(self, rhs) |
a / b | Div | div(self, rhs) |
a % b | Rem | rem(self, rhs) |
-a | Neg | neg(self) |
!a | Not | not(self) |
a += b | AddAssign | add_assign(&mut self, rhs) |
a[i] | Index, IndexMut | index(&self, index) |
a == b, a != b | PartialEq (std::cmp) | eq(&self, other) |
a < b, a >= b | PartialOrd (std::cmp) | partial_cmp(&self, other) |
The other compound assignments follow the same pattern (SubAssign for -=,
MulAssign for *=, and so on). Operators like &&, ||, = and .
can't be overloaded. Only overload an operator when its meaning is obvious for
the type: adding two vectors is, "adding" two users probably isn't.
Calling a method by its full name
Two traits may each give a type a method with the same name. Nothing stops a type from implementing both:
trait Network {
fn status(&self) -> String;
fn kind() -> String;
}
trait Hardware {
fn status(&self) -> String;
fn kind() -> String;
}
struct Printer {
ip: String,
paper: u32,
}
impl Network for Printer {
fn status(&self) -> String {
format!("online at {}", self.ip)
}
fn kind() -> String {
String::from("network device")
}
}
impl Hardware for Printer {
fn status(&self) -> String {
format!("{} sheets left", self.paper)
}
fn kind() -> String {
String::from("peripheral")
}
}
fn main() {
let printer = Printer {
ip: String::from("10.0.0.7"),
paper: 42,
};
println!("{}", Network::status(&printer));
println!("{}", Hardware::status(&printer));
println!("{}", <Printer as Network>::kind());
println!("{}", <Printer as Hardware>::kind());
}online at 10.0.0.7
42 sheets left
network device
peripheralprinter.status() would be ambiguous, so the calls name the trait:
Network::status(&printer) calls the method as a plain function and passes
the receiver explicitly. That works for methods, since the type of the
argument tells the compiler which implementation to use.
kind has no self, so Network::kind() alone gives the compiler nothing to
go on. The fully qualified syntax <Type as Trait>::function() names both
the type and the trait. You rarely need it, but it is the answer whenever the
compiler says it can't tell which implementation you mean. If a type has an
inherent method (one in a plain impl Printer block) with the same name as a
trait method, printer.status() picks the inherent one.
Common mistakes
Using an operator the type doesn't implement
#[derive(Debug)]
struct Money {
cents: u64,
}
fn main() {
let price = Money { cents: 1250 };
let tip = Money { cents: 200 };
println!("{:?}", price + tip);
}Deriving Debug or PartialEq doesn't give a type arithmetic. + needs an
impl Add for Money, and the compiler's note points you at it. Add can't be
derived, since the compiler can't guess what adding two values of your type
means.
Leaving out the associated type
use std::ops::Add;
struct Money {
cents: u64,
}
impl Add for Money {
fn add(self, rhs: Money) -> Money {
Money {
cents: self.cents + rhs.cents,
}
}
}
fn main() {
let total = Money { cents: 1250 } + Money { cents: 200 };
println!("{}", total.cents);
}An associated type is a required item of the trait, just like a required
method. Add type Output = Money; to the impl block.
Using a value after an operator moved it
use std::ops::Add;
#[derive(Debug)]
struct Money {
cents: u64,
}
impl Add for Money {
type Output = Money;
fn add(self, rhs: Money) -> Money {
Money {
cents: self.cents + rhs.cents,
}
}
}
fn main() {
let price = Money { cents: 1250 };
let tip = Money { cents: 200 };
let total = price + tip;
println!("{total:?} from {price:?}");
}add takes self by value, so price + tip moves both operands into it,
exactly like any other function taking ownership. For a small type made of
Copy fields, derive Clone, Copy and the values are copied instead, as
with Vec2 above. For a larger type, implement the operator for references
too (impl Add<&Money> for &Money) and write &price + &tip.
Forgetting the supertrait
use std::fmt;
trait Alert: fmt::Display {
fn level(&self) -> u8;
}
struct HostDown {
host: String,
}
impl Alert for HostDown {
fn level(&self) -> u8 {
2
}
}
fn main() {
let alert = HostDown {
host: String::from("db-2"),
};
println!("{}", alert.host);
}Implementing Alert promises that the type is also Display, and the
compiler checks the promise at the impl. Implementing Alert is not enough
on its own: add an impl fmt::Display for HostDown as well.
Calling an ambiguous method
trait Network {
fn status(&self) -> String;
}
trait Hardware {
fn status(&self) -> String;
}
struct Printer;
impl Network for Printer {
fn status(&self) -> String {
String::from("online")
}
}
impl Hardware for Printer {
fn status(&self) -> String {
String::from("jammed")
}
}
fn main() {
println!("{}", Printer.status());
}The compiler lists both candidates and suggests the fix: name the trait, as in
Network::status(&Printer).
Summary
- An associated type (
type Item;) is a type that each implementation of a trait picks once. Refer to it asSelf::Itemin the trait andT::Itemin generic code, and constrain it withTrait<Item = Type>. - Use an associated type when there is one answer per implementing type, and a generic parameter when a type should implement the trait many times.
trait A: BmakesBa supertrait: implementors ofAmust implementBas well, andA's methods and users can rely onB's methods.- Operators are trait methods from
std::opsandstd::cmp.Add<Rhs = Self>has a default type parameter for the right-hand side and an associatedOutputfor the result. - Binary operators take their operands by value, so non-
Copyvalues are moved. - When two traits give a type methods with the same name, call
Trait::method(&value), or<Type as Trait>::function()for functions withoutself.
Practise what you read
Challenges that use the ideas from this lesson.