A trait is a named set of methods that a type can promise to provide. Display is the promise "I can be formatted with {}". Iterator is the promise "I can hand out items one at a time". Clone is the promise "I can make a deep copy of myself". Traits are how Rust shares behaviour between types, and they replace three features you may know from other languages: Java's interfaces, C++'s abstract base classes and Python's duck typing.
Traits sit at the centre of Rust. Every generic bound you met in lesson 11, every operator such as + and ==, every for loop and every {} in a format string works through a trait. This lesson covers defining and implementing traits, default methods, trait bounds and where clauses, impl Trait in argument and return position, trait objects (dyn Trait) and how they differ from generics, the orphan rule, and the standard traits every Rust programmer uses daily, along with derive. Interviewers commonly ask you to compare static and dynamic dispatch, to explain why some traits cannot be used as dyn Trait, and to explain the orphan rule; you will be able to answer all three.
Defining and implementing a trait
A trait definition lists method signatures. A type then implements the trait in an impl Trait for Type block by providing those methods. A trait can also give a method a body, called a default method, which implementors get for free and may override.
trait Summary {
fn author(&self) -> String;
fn summarise(&self) -> String {
format!("(Read more from {}...)", self.author())
}
}
struct Article {
title: String,
writer: String,
words: u32,
}
struct Post {
handle: String,
text: String,
}
impl Summary for Article {
fn author(&self) -> String {
self.writer.clone()
}
fn summarise(&self) -> String {
format!("{}, by {} ({} words)", self.title, self.writer, self.words)
}
}
impl Summary for Post {
fn author(&self) -> String {
format!("@{}", self.handle)
}
}
fn main() {
let article = Article {
title: String::from("Monsoon arrives early"),
writer: String::from("Leela Menon"),
words: 820,
};
let post = Post {
handle: String::from("rustacean_in"),
text: String::from("Traits finally clicked today"),
};
println!("{}", article.summarise());
println!("{}", post.summarise());
println!("post text: {}", post.text);
}
Output:
Monsoon arrives early, by Leela Menon (820 words)
(Read more from @rustacean_in...)
post text: Traits finally clicked today
Step by step:
trait Summarydeclares two methods.authorends with;, so it is a required method: every implementor must write it.summarisehas a body, so it is a default method.impl Summary for Articleprovidesauthorand also overridessummarisewith its own version.impl Summary for Postprovides onlyauthor, soPostuses the defaultsummarise, which callsself.author(). A default method can call required methods, even though the trait does not know how each type will implement them.- Calls look like ordinary method calls:
article.summarise(). The trait must be in scope (defined in or imported into the current module withuse) for its methods to be callable, which is why you sometimes needuse std::io::Write;before callingwrite_all.
trait Summary
/ | \
author() summarise() (default body calls author())
| |
Article: own Article: own (override)
Post: own Post: default
Compared with other languages:
| Java interface | Python duck typing | Rust trait | |
|---|---|---|---|
| Declared where | On the class: class A implements I | Nowhere | In a separate impl I for A block |
| Can add to existing types | No | Monkey-patching at run time | Yes, within the orphan rule |
| Default methods | Yes (since Java 8) | Inherited from base class | Yes |
| Checked | At compile time | At run time, when called | At compile time |
| Fields | Constants only | Any | None; traits describe behaviour only |
The second row is a real difference. Because implementations are separate from type definitions, you can implement your own trait for types you did not write, including i32 and String, as later examples show.
Reading the compiler error: E0046, a missing required method
Forget a required method and the compiler lists what is missing. This does not compile:
trait Summary {
fn author(&self) -> String;
fn summarise(&self) -> String {
format!("(Read more from {}...)", self.author())
}
}
struct Post {
handle: String,
}
impl Summary for Post {
fn summarise(&self) -> String {
format!("post by @{}", self.handle)
}
}
fn main() {
let post = Post {
handle: String::from("rustacean_in"),
};
println!("{}", post.summarise());
}
error[E0046]: not all trait items implemented, missing: `author`
--> missing_err.rs:13:1
|
2 | fn author(&self) -> String;
| --------------------------- `author` from trait
...
13 | impl Summary for Post {
| ^^^^^^^^^^^^^^^^^^^^^ missing `author` in implementation
The message names every missing item, points at its declaration in the trait, and points at the impl block that lacks it. Overriding summarise does not excuse you from writing author: a default method is optional, a required one never is. In an editor with rust-analyzer, the "implement missing members" quick fix writes the stubs for you.
Traits as parameters: bounds, impl Trait and where
Lesson 11 introduced trait bounds on generic functions. With your own traits, the same syntax lets a function accept any type that implements the trait. Rust offers three spellings:
use std::fmt::{Debug, Display};
trait Summary {
fn summarise(&self) -> String;
}
#[derive(Debug)]
struct Post {
handle: String,
likes: u32,
}
impl Summary for Post {
fn summarise(&self) -> String {
format!("@{} ({} likes)", self.handle, self.likes)
}
}
impl Summary for i32 {
fn summarise(&self) -> String {
format!("the number {self}")
}
}
// Three ways to write the same bound.
fn notify(item: &impl Summary) {
println!("Breaking: {}", item.summarise());
}
fn notify_generic<T: Summary>(item: &T) {
println!("Generic: {}", item.summarise());
}
fn notify_where<T>(item: &T)
where
T: Summary + Debug,
{
println!("Where: {} / {item:?}", item.summarise());
}
// Two parameters that must be the same type.
fn both<T: Summary>(a: &T, b: &T) -> String {
format!("{} & {}", a.summarise(), b.summarise())
}
// Two parameters that may be different types.
fn either(a: &impl Summary, b: &impl Summary) -> String {
format!("{} | {}", a.summarise(), b.summarise())
}
fn labelled<T: Summary + Display>(item: &T) -> String {
format!("{item} => {}", item.summarise())
}
fn main() {
let post = Post {
handle: String::from("asha"),
likes: 12,
};
notify(&post);
notify_generic(&7);
notify_where(&post);
println!("{}", both(&1, &2));
println!("{}", either(&post, &3));
println!("{}", labelled(&42));
}
Output:
Breaking: @asha (12 likes)
Generic: the number 7
Where: @asha (12 likes) / Post { handle: "asha", likes: 12 }
the number 1 & the number 2
@asha (12 likes) | the number 3
42 => the number 42
| Form | Example | Notes |
|---|---|---|
impl Trait in argument position | fn notify(item: &impl Summary) | Shortest; each impl Trait parameter is its own anonymous type parameter |
| Generic with inline bound | fn notify<T: Summary>(item: &T) | Lets you name T and reuse it |
where clause | fn notify<T>(item: &T) where T: Summary + Debug | Same meaning, easier to read when bounds are long |
notify and notify_generic compile to the same thing: a monomorphised copy per type, exactly as in lesson 11. The difference shows when there are two parameters:
both<T: Summary>(a: &T, b: &T)uses one name, so both arguments must be the same type.either(a: &impl Summary, b: &impl Summary)has two anonymous parameters, so the arguments may be different types; the call passes aPostand ani32.
Notice impl Summary for i32. You may implement your own trait for a standard type, and from then on 7.summarise() works anywhere the trait is in scope.
T: Summary + Display requires both traits. labelled(&42) works because i32 implements Display from the standard library and Summary from this program.
impl Trait in return position
impl Trait can also appear as a return type. It means "this function returns one specific type that implements the trait, but I am not telling you which". The caller can use only the trait's methods.
fn evens_up_to(limit: u32) -> impl Iterator<Item = u32> {
(0..=limit).filter(|n| n % 2 == 0)
}
fn greeting(name: &str) -> impl std::fmt::Display {
format!("Namaste, {name}!")
}
fn main() {
let evens: Vec<u32> = evens_up_to(10).collect();
println!("{evens:?}");
println!("{}", greeting("Kiran"));
}
Output:
[0, 2, 4, 6, 8, 10]
Namaste, Kiran!
The real return type of evens_up_to is something like Filter<RangeInclusive<u32>, {closure}>. Closures have types that cannot be written in source code at all, so impl Iterator<Item = u32> is not just shorter, it is the only way to return this iterator without boxing it. Item = u32 names the iterator's associated type, explained later in this lesson. Lesson 14 uses this pattern a lot.
The returned value is still a concrete type known to the compiler, so calls are static and can be inlined. The type is only hidden from the caller, which keeps your freedom to change it later without breaking their code. Since Rust 1.75 you can also write -> impl Trait in methods inside trait definitions.
Reading the compiler error: E0308, impl Trait must be one type
"Returns one specific type" is a real restriction. This does not compile:
use std::fmt::Display;
fn label(verbose: bool) -> impl Display {
if verbose {
String::from("a long description")
} else {
42
}
}
fn main() {
println!("{}", label(true));
}
error[E0308]: `if` and `else` have incompatible types
--> impl_return_err.rs:7:9
|
4 | / if verbose {
5 | | String::from("a long description")
| | ---------------------------------- expected because of this
6 | | } else {
7 | | 42
| | ^^ expected `String`, found integer
8 | | }
| |_____- `if` and `else` have incompatible types
|
help: you could change the return type to be a boxed trait object
|
3 - fn label(verbose: bool) -> impl Display {
3 + fn label(verbose: bool) -> Box<dyn Display> {
|
help: if you change the return type to expect trait objects, box the returned expressions
|
5 ~ Box::new(String::from("a long description"))
6 | } else {
7 ~ Box::new(42)
|
help: try using a conversion method
|
7 | 42.to_string()
| ++++++++++++
Both String and i32 implement Display, so it looks as if this should work. But impl Display stands for a single hidden type, and the two branches produce different ones, so the if expression has no single type. The compiler offers two fixes and both are worth knowing:
- Return a trait object,
Box<dyn Display>, which can hold either type at the cost of a heap allocation and dynamic dispatch. The next section explains it. - Convert both branches to the same type, here with
42.to_string().
Trait objects: dyn Trait
Generics give you one compiled copy per type, and a Vec<T> holds only one T. Sometimes you need the opposite: a single collection holding circles and rectangles together, or a function that works on any shape chosen at run time. For that, Rust has trait objects, written dyn Trait.
use std::mem::size_of;
trait Shape {
fn area(&self) -> f64;
fn name(&self) -> &str;
}
struct Circle {
radius: f64,
}
struct Rect {
w: f64,
h: f64,
}
impl Shape for Circle {
fn area(&self) -> f64 {
std::f64::consts::PI * self.radius * self.radius
}
fn name(&self) -> &str {
"circle"
}
}
impl Shape for Rect {
fn area(&self) -> f64 {
self.w * self.h
}
fn name(&self) -> &str {
"rect"
}
}
fn total_area(shapes: &[Box<dyn Shape>]) -> f64 {
shapes.iter().map(|s| s.area()).sum()
}
fn print_static<T: Shape>(shape: &T) {
println!("static: {} {:.2}", shape.name(), shape.area());
}
fn print_dynamic(shape: &dyn Shape) {
println!("dynamic: {} {:.2}", shape.name(), shape.area());
}
fn main() {
let shapes: Vec<Box<dyn Shape>> = vec![
Box::new(Circle { radius: 1.0 }),
Box::new(Rect { w: 3.0, h: 4.0 }),
Box::new(Circle { radius: 2.0 }),
];
for s in &shapes {
println!("{:<6} area {:>6.2}", s.name(), s.area());
}
println!("total area {:.2}", total_area(&shapes));
let r = Rect { w: 2.0, h: 5.0 };
print_static(&r);
print_dynamic(&r);
println!(
"&Rect = {} bytes, &dyn Shape = {} bytes, Box<dyn Shape> = {} bytes",
size_of::<&Rect>(),
size_of::<&dyn Shape>(),
size_of::<Box<dyn Shape>>()
);
}
Output:
circle area 3.14
rect area 12.00
circle area 12.57
total area 27.71
static: rect 10.00
dynamic: rect 10.00
&Rect = 8 bytes, &dyn Shape = 16 bytes, Box<dyn Shape> = 16 bytes
Vec<Box<dyn Shape>>holds values of different types. Each element is aBox, a pointer to a value on the heap (lesson 15), and thedyn Shapepart says "some type that implementsShape".- Circles have area πr²: π × 1 = 3.14 and π × 4 = 12.57. The rectangle is 3 × 4 = 12. The total is 5π + 12 = 27.71.
print_staticis generic: the compiler generatesprint_static::<Rect>and callsRect::areadirectly.print_dynamictakes&dyn Shape: one compiled copy, and the method to call is looked up at run time.
Why dyn Trait needs a pointer, and what the pointer holds
dyn Shape is unsized: a Circle and a Rect have different sizes, so the compiler cannot know how many bytes a dyn Shape takes. You can only use it behind a pointer: &dyn Shape, &mut dyn Shape, Box<dyn Shape>, Rc<dyn Shape> and so on.
That pointer is a fat pointer, two machine words: one points at the data, the other at a vtable (virtual method table), a static table the compiler creates once for each (type, trait) pair. The output confirms it: &Rect is 8 bytes, while &dyn Shape and Box<dyn Shape> are 16 bytes on a 64-bit machine.
Box<dyn Shape> (16 bytes) vtable for (Circle, Shape)
+-----------+-----------+ +------------------------+
| data ptr | vtable ptr|--------> | drop_in_place::<Circle>|
+-----------+-----------+ | size = 8, align = 8 |
| | area -> Circle::area |
v | name -> Circle::name |
heap: Circle { radius: 1.0 } +------------------------+
s.area() => load vtable ptr, load the "area" slot, call it
A call such as s.area() reads the vtable pointer, loads the area entry and makes an indirect call. That is dynamic dispatch. Java's virtual methods and C++'s virtual functions work the same way, but there the vtable pointer lives inside every object. In Rust it lives in the pointer, so a plain Circle carries no extra word and pays nothing unless you turn it into a trait object.
Static vs dynamic dispatch
Generics / impl Trait (static) | dyn Trait (dynamic) | |
|---|---|---|
| Which method runs | Decided at compile time | Looked up in the vtable at run time |
| Compiled copies | One per concrete type | One |
| Inlining | Possible | Usually not, through the vtable |
| Mixed types in one collection | No | Yes |
| Pointer size | Normal | Fat: data pointer plus vtable pointer |
| Binary size and compile time | Grow with the number of types | Smaller |
| Trait requirements | Any trait | The trait must be dyn compatible |
Choose generics by default; they are faster and work with every trait. Choose dyn Trait when you need different types together at run time (plug-ins, UI widgets, a list of handlers), when the set of types is open-ended, or when the code size of many instantiations matters more than the cost of an indirect call. If the set of types is small and fixed, an enum with one variant per type is often simpler than either (lesson 7).
Interview tip
For "static vs dynamic dispatch", give the mechanism and the trade-off in one breath: "Generics are monomorphised, so each call goes straight to the right function and can be inlined, at the cost of code size. dyn Trait uses a fat pointer with a vtable; one copy of the code handles all types, and each call is an indirect jump. Use generics by default and dyn for heterogeneous collections or plug-in style code."
Reading the compiler error: E0038, a trait that is not dyn compatible
Not every trait can be a trait object. A trait is dyn compatible (formerly called object safe) only if a vtable can be built for it. This does not compile:
trait Duplicate {
fn duplicate(&self) -> Self;
}
#[derive(Clone)]
struct Note(String);
impl Duplicate for Note {
fn duplicate(&self) -> Self {
self.clone()
}
}
fn main() {
let items: Vec<Box<dyn Duplicate>> = vec![Box::new(Note(String::from("hi")))];
println!("{}", items.len());
}
error[E0038]: the trait `Duplicate` is not dyn compatible
--> dyn_err.rs:15:28
|
15 | let items: Vec<Box<dyn Duplicate>> = vec![Box::new(Note(String::from("hi")))];
| ^^^^^^^^^ `Duplicate` 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>
--> dyn_err.rs:2:28
|
1 | trait Duplicate {
| --------- this trait is not dyn compatible...
2 | fn duplicate(&self) -> Self;
| ^^^^ ...because method `duplicate` references the `Self` type in its return type
= help: consider moving `duplicate` to another trait
= help: only type `Note` implements `Duplicate`; consider using it directly instead.
The note explains why. duplicate returns Self, meaning "the same type as the implementor". Behind a dyn Duplicate, that type is unknown, so the caller would not know how many bytes the return value takes. The main rules for dyn compatibility are:
- Every method callable through
dyntakesselfbehind a pointer (&self,&mut self,self: Box<Self>and similar) and does not mentionSelfanywhere else in its signature, so it cannot returnSelf. - Such methods have no type parameters of their own (a vtable slot cannot hold infinitely many monomorphised copies) and do not return
impl Trait. - The trait does not require
Self: Sizedand has no associated constants.
A method or associated function that breaks a rule, such as a constructor fn new() -> Self, can be excluded from the vtable by adding where Self: Sized to it; it is then callable on concrete types but not through dyn. This is exactly why Clone is not dyn compatible (its clone returns Self), while Display and Iterator are.
The orphan rule
You can implement a trait for a type only if the trait or the type is defined in your crate (a crate is a compiled package, lesson 10). This is called the orphan rule, and it is part of a broader property called coherence: for any type and trait, there is at most one implementation in the whole program.
This does not compile, because both Display and Vec come from the standard library:
use std::fmt;
impl fmt::Display for Vec<String> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "[{}]", self.join(", "))
}
}
fn main() {
let names = vec![String::from("a"), String::from("b")];
println!("{names}");
}
error[E0117]: only traits defined in the current crate can be implemented for types defined outside of the crate
--> orphan_err.rs:3:1
|
3 | impl fmt::Display for Vec<String> {
| ^^^^^^^^^^^^^^^^^^^^^^-----------
| |
| `Vec` is not defined in the current crate
|
= note: impl doesn't have any local type before any uncovered type parameters
= note: for more information see https://doc.rust-lang.org/reference/items/implementations.html#orphan-rules
= note: define and implement a trait or new type instead
Why have this rule? Imagine two libraries you depend on both implement Display for Vec<String> differently. Which one would {} use? Worse, the standard library adding such an implementation in a later release would break both libraries. The orphan rule makes those conflicts impossible.
type is yours type is foreign
trait is yours allowed allowed (Shout for String)
trait is foreign allowed NOT allowed (Display for Vec)
(Display for NameList)
The standard workaround is the newtype pattern from lesson 6: wrap the foreign type in a local tuple struct, and implement the trait for the wrapper.
use std::fmt;
struct NameList(Vec<String>);
impl fmt::Display for NameList {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "[{}]", self.0.join(", "))
}
}
trait Shout {
fn shout(&self) -> String;
}
impl Shout for String {
fn shout(&self) -> String {
self.to_uppercase() + "!"
}
}
fn main() {
let names = NameList(vec![String::from("Anu"), String::from("Bala")]);
println!("{names}");
println!("{}", String::from("hello").shout());
}
Output:
[Anu, Bala]
HELLO!
NameList is local, so Display for NameList is allowed. Shout for String is also allowed because Shout is local. The wrapper costs nothing at run time. Its downside is that NameList does not have Vec's methods, so you reach them through .0, or implement Deref (lesson 15) when the wrapper should behave like the inner type.
The standard traits you will use every day
Most of the traits you use come from the standard library. Several can be generated with #[derive(...)], which writes a field-by-field implementation for you.
| Trait | What it gives you | Derivable | Notes |
|---|---|---|---|
Debug | {:?} and {:#?} | Yes | For programmers; derive it on almost everything |
Display | {} and .to_string() | No | For users; you decide the format |
Clone | .clone(), an explicit copy | Yes | Can be expensive (deep copy) |
Copy | Implicit bitwise copy instead of move | Yes | Requires Clone; only for types with no heap ownership and no Drop |
PartialEq | == and != | Yes | Equality need not be reflexive (f64::NAN != f64::NAN) |
Eq | Marker: equality is a true equivalence | Yes | Requires PartialEq; floats do not implement it |
PartialOrd | <, >, <=, >= and partial_cmp | Yes | Some pairs may be incomparable (NaN) |
Ord | cmp, total ordering; needed by sort and BTreeMap | Yes | Requires Eq and PartialOrd |
Hash | Feeding a value to a hasher; needed by HashMap keys | Yes | Must agree with Eq: equal values give equal hashes |
Default | Type::default() | Yes | Zero, empty or false for every field when derived |
From / Into | Infallible conversions between types | No | Implement From; Into comes for free |
Iterator | next() and dozens of adapter methods | No | Lesson 14 |
Deriving comparison and hashing
use std::collections::{HashMap, HashSet};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
enum Level {
Low,
Medium,
High,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Default)]
struct Version {
major: u32,
minor: u32,
patch: u32,
}
fn main() {
let mut versions = vec![
Version {
major: 1,
minor: 10,
patch: 0,
},
Version {
major: 1,
minor: 2,
patch: 7,
},
Version {
major: 0,
minor: 9,
patch: 9,
},
];
versions.sort();
println!("{versions:?}");
println!("max = {:?}", versions.iter().max());
println!("default = {:?}", Version::default());
let a = Version {
major: 1,
minor: 2,
patch: 7,
};
let b = a.clone();
println!("a == b: {}, a < latest: {}", a == b, a < versions[2]);
let unique: HashSet<Version> = versions.iter().cloned().chain([a, b]).collect();
println!("unique versions: {}", unique.len());
let mut counts: HashMap<Level, u32> = HashMap::new();
for level in [Level::High, Level::Low, Level::High] {
*counts.entry(level).or_insert(0) += 1;
}
let level = Level::Medium;
let copy = level;
println!(
"High seen {} times; {level:?} > Low: {}",
counts[&Level::High],
copy > Level::Low
);
}
Output:
[Version { major: 0, minor: 9, patch: 9 }, Version { major: 1, minor: 2, patch: 7 }, Version { major: 1, minor: 10, patch: 0 }]
max = Some(Version { major: 1, minor: 10, patch: 0 })
default = Version { major: 0, minor: 0, patch: 0 }
a == b: true, a < latest: true
unique versions: 3
High seen 2 times; Medium > Low: true
What the derives did:
- Derived
PartialOrdandOrdon a struct compare field by field in declaration order, like comparing words letter by letter. That is exactly semantic-version order here: 0.9.9, then 1.2.7, then 1.10.0, becauseminor2 is less than 10 whenmajoris equal. Comparing the strings "1.10.0" and "1.2.7" would get this wrong. - On an enum, derived ordering follows the order the variants are written, so
Low < Medium < High. HashandEqallowVersionin aHashSetandLevelas aHashMapkey. The set has 3 entries becauseaandbequal the existing 1.2.7.LevelisCopy, solet copy = level;copies, andlevelis still usable in the next line.
Reading the compiler error: E0277, a field that cannot be Eq or Hash
Derives work field by field, so every field must implement the trait. Floating-point fields are the classic trap. This does not compile:
use std::collections::HashSet;
#[derive(Debug, PartialEq, Eq, Hash)]
struct Reading {
sensor: u32,
celsius: f64,
}
fn main() {
let mut seen = HashSet::new();
seen.insert(Reading {
sensor: 1,
celsius: 21.5,
});
println!("{}", seen.len());
}
error[E0277]: the trait bound `f64: Eq` is not satisfied
--> eq_err.rs:6:5
|
3 | #[derive(Debug, PartialEq, Eq, Hash)]
| -- in this derive macro expansion
...
6 | celsius: f64,
| ^^^^^^^^^^^^ the trait `Eq` is not implemented for `f64`
error[E0277]: the trait bound `f64: Hash` is not satisfied
--> eq_err.rs:6:5
|
3 | #[derive(Debug, PartialEq, Eq, Hash)]
| ---- in this derive macro expansion
...
6 | celsius: f64,
| ^^^^^^^^^^^^ the trait `Hash` is not implemented for `f64`
f64 implements PartialEq but not Eq, because NaN is not equal to itself, which breaks the rule that every value equals itself. It does not implement Hash either. A hash set built on such a key could lose values. The help list (trimmed here) shows the integer types that do implement Eq, which hints at the usual fix: store the reading as an integer in a fixed unit, such as hundredths of a degree.
use std::collections::HashSet;
#[derive(Debug, PartialEq, Eq, Hash)]
struct Reading {
sensor: u32,
centi_celsius: i32,
}
fn main() {
let mut seen = HashSet::new();
seen.insert(Reading {
sensor: 1,
centi_celsius: 2150,
});
seen.insert(Reading {
sensor: 1,
centi_celsius: 2150,
});
seen.insert(Reading {
sensor: 2,
centi_celsius: -40,
});
println!("{} unique readings", seen.len());
let r = seen.iter().find(|r| r.sensor == 2).unwrap();
println!("sensor 2: {:.2} C", f64::from(r.centi_celsius) / 100.0);
}
Output:
2 unique readings
sensor 2: -0.40 C
The two identical readings for sensor 1 collapse into one entry, so the set holds 2. If you really need floats as keys, wrap them in a newtype that defines its own Eq and Hash using f64::to_bits and decides how to treat NaN.
Display, From, Into and Default by hand
use std::fmt;
#[derive(Debug, Clone, Copy)]
struct Celsius(f64);
#[derive(Debug, Clone, Copy)]
struct Fahrenheit(f64);
impl From<Celsius> for Fahrenheit {
fn from(c: Celsius) -> Self {
Fahrenheit(c.0 * 9.0 / 5.0 + 32.0)
}
}
impl fmt::Display for Fahrenheit {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{:.1} F", self.0)
}
}
struct Config {
name: String,
retries: u8,
verbose: bool,
}
impl Default for Config {
fn default() -> Self {
Config {
name: String::from("app"),
retries: 3,
verbose: false,
}
}
}
fn main() {
let body = Celsius(37.0);
let f1 = Fahrenheit::from(body);
let f2: Fahrenheit = Celsius(100.0).into();
println!("{f1} and {f2}");
let s: String = "owned".into();
let n: i64 = 7_i32.into();
let big = u64::from(250_u8);
println!("{s} {n} {big}");
let cfg = Config {
verbose: true,
..Default::default()
};
println!(
"{} retries={} verbose={}",
cfg.name, cfg.retries, cfg.verbose
);
}
Output:
98.6 F and 212.0 F
owned 7 250
app retries=3 verbose=true
impl From<Celsius> for Fahrenheitdefines one conversion. 37 × 9 / 5 + 32 = 98.6, and 100 × 9 / 5 + 32 = 212.Celsius(100.0).into()works without any extra code, because the standard library has a blanket implementation: for every pair whereU: From<T>, it implementsInto<U> for T. That is why you implementFromand getIntofree, never the other way round. The annotationlet f2: Fahrenheittellsintowhich type to produce."owned".into()usesFrom<&str> for String;7_i32.into()toi64andu64::from(250_u8)use the lossless numeric conversions. There is noFrom<i64> for i32, because that could lose data; the fallible version isTryFrom.impl Default for Configchooses real defaults (3 retries, name "app") rather than the derived zeros, and..Default::default()fills the fields not listed.
Operators are traits
Every operator in Rust is a trait in std::ops or std::cmp. Implementing Add makes + work for your type:
use std::ops::Add;
#[derive(Debug, Clone, Copy, PartialEq)]
struct Rupees(u64);
impl Add for Rupees {
type Output = Rupees;
fn add(self, other: Rupees) -> Rupees {
Rupees(self.0 + other.0)
}
}
fn main() {
let bill = Rupees(850) + Rupees(395) + Rupees(55);
println!("{bill:?}");
println!("{}", bill == Rupees(1300));
}
Output:
Rupees(1300)
true
850 + 395 + 55 = 1,300. type Output = Rupees; is an associated type: a type that each implementation chooses, here the result type of +. Rust has no other operator overloading: you cannot invent new operators, and && and || cannot be overloaded.
Implementing Iterator: associated types in action
Iterator has one required method, next, and one associated type, Item. Implement those two and you get every adapter (map, filter, sum, collect and the rest) as default methods:
struct Countdown {
remaining: u32,
}
impl Iterator for Countdown {
type Item = u32;
fn next(&mut self) -> Option<u32> {
if self.remaining == 0 {
None
} else {
self.remaining -= 1;
Some(self.remaining + 1)
}
}
}
fn main() {
for n in (Countdown { remaining: 3 }) {
println!("{n}...");
}
let total: u32 = Countdown { remaining: 5 }.sum();
let odd: Vec<u32> = Countdown { remaining: 6 }.filter(|n| n % 2 == 1).collect();
println!("sum = {total}, odd = {odd:?}");
}
Output:
3...
2...
1...
sum = 15, odd = [5, 3, 1]
5 + 4 + 3 + 2 + 1 = 15, and the odd values from a countdown starting at 6 are 5, 3 and 1. One implementation of next unlocked for loops, sum, filter and collect.
Why is Item an associated type rather than a type parameter, as in Iterator<T>? Because a type should have exactly one item type: a Countdown yields u32, full stop. A generic trait such as From<T> is the opposite: a type can implement it many times, From<Celsius>, From<Kelvin>, and so on. Rule of thumb: one implementation per type, use an associated type; many, use a type parameter.
Supertraits and blanket implementations
Two more tools appear throughout the standard library.
A supertrait is a trait that another trait requires. trait Describe: Display means "to implement Describe, a type must also implement Display", and in return the default methods of Describe may use {} on self.
A blanket implementation implements a trait for every type that meets a bound: impl<T: Display> Loud for T. The standard library's ToString works this way, which is why any Display type has .to_string().
use std::fmt::{self, Display};
trait Describe: Display {
fn describe(&self) -> String {
format!("<<{self}>>")
}
}
trait Loud {
fn loud(&self) -> String;
}
impl<T: Display> Loud for T {
fn loud(&self) -> String {
self.to_string().to_uppercase()
}
}
struct City {
name: String,
}
impl Display for City {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "City of {}", self.name)
}
}
impl Describe for City {}
fn main() {
let c = City {
name: String::from("Mysuru"),
};
println!("{}", c.describe());
println!("{}", c.loud());
println!("{}", 3.5.loud());
println!("{}", "quiet".loud());
}
Output:
<<City of Mysuru>>
CITY OF MYSURU
3.5
QUIET
impl Describe for City {} is empty, because the only method has a default. loud works on City, on the float 3.5 and on a &str, without any of them implementing Loud directly. Upper-casing "3.5" leaves it unchanged.
Supertraits are not inheritance. A City is not "a kind of Display"; it simply has to implement both traits, and there are no fields or overriding chains involved.
Idioms and pitfalls
Idiom: derive the common traits early
For a plain data type, start with #[derive(Debug, Clone, PartialEq)], and add Eq, Hash, PartialOrd, Ord, Default or Copy when they make sense. Library authors in particular should implement common traits, because users cannot add them later: the orphan rule stops them.
Idiom: accept impl Trait, return concrete types or impl Trait
Take impl AsRef<str>, impl Into<String> or impl Iterator<Item = T> in parameters to make functions flexible for callers. Return a concrete type when it is simple to name, and impl Trait when it is not (iterators and closures).
Pitfall: reaching for Box<dyn Trait> out of habit
Programmers from Java often use trait objects everywhere because they resemble interface references. In Rust, generics are the default; trait objects cost a heap allocation (for Box) and an indirect call, and they rule out traits that are not dyn compatible. Use them when you need mixed types at run time.
Pitfall: Ord and PartialOrd that disagree
If you implement Ord by hand, implement PartialOrd as Some(self.cmp(other)), and make sure PartialEq agrees with them. Sorting, BTreeMap and binary search assume these are consistent; if they are not, results may be wrong in confusing ways, though never memory-unsafe.
Pitfall: method not found because the trait is not imported
If file.write_all(...) fails with E0599 "no method named write_all found", the type probably does implement the trait, but the trait is not in scope. The compiler's help line suggests the use statement, such as use std::io::Write;.
Exercises
Exercise 1: shapes with a default method
Define a Shape trait with required area and perimeter methods and a default describe method that prints both. Implement it for Rectangle and Circle, store several shapes in a Vec<Box<dyn Shape>>, and write a function that returns the shape with the largest area.
Solution
largest_area returns Option<&dyn Shape> borrowed from the vector; s.as_ref() turns a &Box<dyn Shape> into a &dyn Shape. is_none_or (stable since Rust 1.82) is true for None or when the closure returns true.
trait Shape {
fn area(&self) -> f64;
fn perimeter(&self) -> f64;
fn describe(&self) -> String {
format!("area {:.2}, perimeter {:.2}", self.area(), self.perimeter())
}
}
struct Rectangle {
w: f64,
h: f64,
}
struct Circle {
r: f64,
}
impl Shape for Rectangle {
fn area(&self) -> f64 {
self.w * self.h
}
fn perimeter(&self) -> f64 {
2.0 * (self.w + self.h)
}
}
impl Shape for Circle {
fn area(&self) -> f64 {
std::f64::consts::PI * self.r * self.r
}
fn perimeter(&self) -> f64 {
2.0 * std::f64::consts::PI * self.r
}
}
fn largest_area(shapes: &[Box<dyn Shape>]) -> Option<&dyn Shape> {
let mut best: Option<&dyn Shape> = None;
for s in shapes {
if best.is_none_or(|b| s.area() > b.area()) {
best = Some(s.as_ref());
}
}
best
}
fn main() {
let shapes: Vec<Box<dyn Shape>> = vec![
Box::new(Rectangle { w: 3.0, h: 4.0 }),
Box::new(Circle { r: 2.0 }),
Box::new(Rectangle { w: 1.0, h: 10.0 }),
];
for s in &shapes {
println!("{}", s.describe());
}
if let Some(big) = largest_area(&shapes) {
println!("largest: {}", big.describe());
}
}
area 12.00, perimeter 14.00
area 12.57, perimeter 12.57
area 10.00, perimeter 22.00
largest: area 12.57, perimeter 12.57
The circle of radius 2 has area 4π ≈ 12.57, which beats the 3 × 4 rectangle's 12. Its perimeter is also 4π, so both numbers match.
Exercise 2: a 12-hour clock
Implement Display for Time { hours: u8, minutes: u8 } (24-hour values) so it prints a 12-hour time such as 6:45 PM. Midnight is 12:05 AM and noon is 12:00 PM.
Solution
use std::fmt;
struct Time {
hours: u8,
minutes: u8,
}
impl fmt::Display for Time {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let suffix = if self.hours < 12 { "AM" } else { "PM" };
let h12 = match self.hours % 12 {
0 => 12,
h => h,
};
write!(f, "{h12}:{:02} {suffix}", self.minutes)
}
}
fn main() {
for (hours, minutes) in [(0, 5), (9, 30), (12, 0), (18, 45), (23, 59)] {
let t = Time { hours, minutes };
println!("{hours:02}:{minutes:02} -> {t}");
}
}
00:05 -> 12:05 AM
09:30 -> 9:30 AM
12:00 -> 12:00 PM
18:45 -> 6:45 PM
23:59 -> 11:59 PM
hours % 12 maps 0 and 12 to 0, which the match turns into 12. {:02} pads the minutes to two digits.
Exercise 3: conversions with From
Implement From<(u8, u8, u8)> for an Rgb struct and From<Rgb> for String producing a hex code such as #FF9933. Write paint(colour: impl Into<Rgb>) that accepts either a tuple or an Rgb.
Solution
impl From<Rgb> for String is allowed by the orphan rule because Rgb is local, even though From and String are not. paint accepts an Rgb too, because the standard library implements From<T> for T for every type.
#[derive(Debug, PartialEq)]
struct Rgb {
r: u8,
g: u8,
b: u8,
}
impl From<(u8, u8, u8)> for Rgb {
fn from((r, g, b): (u8, u8, u8)) -> Self {
Rgb { r, g, b }
}
}
impl From<Rgb> for String {
fn from(c: Rgb) -> String {
format!("#{:02X}{:02X}{:02X}", c.r, c.g, c.b)
}
}
fn paint(colour: impl Into<Rgb>) -> String {
let c: Rgb = colour.into();
String::from(c)
}
fn main() {
let saffron: Rgb = (255, 153, 51).into();
println!("{saffron:?}");
println!("{}", paint((19, 136, 8)));
println!("{}", paint(Rgb { r: 0, g: 0, b: 128 }));
}
Rgb { r: 255, g: 153, b: 51 }
#138808
#000080
19 = 0x13, 136 = 0x88, 8 = 0x08, and navy is (0, 0, 128) = #000080.
Exercise 4: a Fibonacci iterator
Implement Iterator for a Fibonacci struct that yields 0, 1, 1, 2, 3, ... and stops, instead of overflowing, when the next term no longer fits in a u64. Print the first 10 terms, the sum of the even terms below 100, and how many terms it yields in total.
Solution
Storing the state as Option<u64> lets the iterator still yield the last terms that fit while marking that the next one would overflow. checked_add returns None on overflow.
struct Fibonacci {
current: Option<u64>,
next: Option<u64>,
}
impl Fibonacci {
fn new() -> Self {
Fibonacci {
current: Some(0),
next: Some(1),
}
}
}
impl Iterator for Fibonacci {
type Item = u64;
fn next(&mut self) -> Option<u64> {
let value = self.current?;
self.current = self.next;
self.next = self.next.and_then(|n| n.checked_add(value));
Some(value)
}
}
fn main() {
let first: Vec<u64> = Fibonacci::new().take(10).collect();
println!("{first:?}");
let even_sum: u64 = Fibonacci::new()
.take_while(|&n| n < 100)
.filter(|n| n % 2 == 0)
.sum();
println!("sum of even terms below 100 = {even_sum}");
println!("terms that fit in u64: {}", Fibonacci::new().count());
println!("largest: {:?}", Fibonacci::new().last());
}
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
sum of even terms below 100 = 44
terms that fit in u64: 94
largest: Some(12200160415121876738)
The even terms below 100 are 0, 2, 8 and 34, which sum to 44. F(0) through F(93) fit in a u64, which is 94 terms; F(94) would exceed 2^64 − 1.
Exercise 5: a custom sort order
Implement Ord for Student { name, marks } so that sorting puts higher marks first and breaks ties by name alphabetically.
Solution
Comparing other.marks with self.marks (reversed) gives descending marks; then_with breaks ties only when the marks are equal.
use std::cmp::Ordering;
#[derive(Debug, PartialEq, Eq)]
struct Student {
name: String,
marks: u32,
}
impl Ord for Student {
fn cmp(&self, other: &Self) -> Ordering {
other
.marks
.cmp(&self.marks)
.then_with(|| self.name.cmp(&other.name))
}
}
impl PartialOrd for Student {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
fn main() {
let mut class = [
Student {
name: String::from("Ravi"),
marks: 88,
},
Student {
name: String::from("Anjali"),
marks: 95,
},
Student {
name: String::from("Dev"),
marks: 88,
},
Student {
name: String::from("Bhavna"),
marks: 72,
},
];
class.sort();
for (rank, s) in class.iter().enumerate() {
println!("{}. {} ({})", rank + 1, s.name, s.marks);
}
}
1. Anjali (95)
2. Dev (88)
3. Ravi (88)
4. Bhavna (72)
Dev and Ravi both have 88, so the name decides: D comes before R. class is an array; sort works on any mutable slice.
Exercise 6: explain the error
Why does impl fmt::Display for Vec<String> fail, while impl Shout for String and impl fmt::Display for NameList both compile?
Solution
The orphan rule requires the trait or the type to be local to your crate. Display and Vec are both from the standard library, so the first is rejected with E0117. Shout is a local trait and NameList is a local type, so each of the other two has a local part. The fix for the first is the newtype NameList(Vec<String>), which the lesson's newtype.rs program compiles and runs.
Interview questions
Q1. What is a trait, and how does it differ from a Java interface?
A trait is a set of method signatures, optionally with default bodies, that types implement. Like an interface it describes behaviour, has default methods and supports dynamic dispatch. Unlike an interface, implementations are written in separate impl blocks, so you can implement your trait for existing types such as i32, and traits are used mainly as compile-time bounds with static dispatch rather than as run-time references.
Q2. What is the difference between static and dynamic dispatch?
With static dispatch (generics, impl Trait) the compiler knows the concrete type, monomorphises the code and calls the method directly, allowing inlining. With dynamic dispatch (dyn Trait) the call goes through a vtable found via a fat pointer, so one copy of the code handles all types at the cost of an indirect call and lost inlining. Static dispatch is the default; dynamic dispatch is for heterogeneous collections and plug-in style code.
Q3. What is a fat pointer?
A pointer that carries extra data alongside the address. &dyn Trait and Box<dyn Trait> hold a data pointer and a vtable pointer; slices such as &[T] and &str hold a data pointer and a length. Both are two words, 16 bytes on a 64-bit machine.
Q4. What makes a trait dyn compatible (object safe)?
A vtable must be buildable for it. Its methods must take self behind a pointer, must not mention Self elsewhere in their signatures (so no returning Self), and must not have their own generic type parameters; the trait must not require Self: Sized or have associated constants. Individual methods can opt out with where Self: Sized. Clone is not dyn compatible because clone returns Self.
Q5. What is the orphan rule, and why does it exist?
You may implement a trait for a type only if the trait or the type is defined in your crate. It guarantees coherence: there is at most one implementation of a trait for a type, so two crates cannot provide conflicting implementations and upstream crates can add implementations without breaking you. The workaround is a local newtype wrapper.
Q6. What is the difference between impl Trait in argument position and in return position?
In argument position it is shorthand for an anonymous generic parameter, so the caller chooses the type. In return position the function chooses one concrete type and hides it from the caller, who can use only the trait's methods. Return-position impl Trait must be a single type on every path, and is needed for unnameable types like closures and iterator chains.
Q7. Why implement From instead of Into?
The standard library has a blanket impl<T, U: From<T>> Into<U> for T, so implementing From gives you Into automatically. Implementing only Into would not give you From. Functions often take impl Into<X> to accept many input types, and ? uses From to convert error types.
Q8. What is the difference between PartialEq and Eq, and between PartialOrd and Ord?
PartialEq provides ==; Eq is a marker promising equality is reflexive, so every value equals itself. PartialOrd allows some values to be incomparable; Ord promises a total order. Floats implement only the partial versions because of NaN, which is why you cannot sort a Vec<f64> with sort() or use f64 as a HashMap key directly; use sort_by with total_cmp or an integer representation instead.
Q9. When would you use an associated type instead of a generic type parameter on a trait?
Use an associated type when each implementing type has exactly one natural choice, as with Iterator::Item or Add::Output; it keeps signatures short and inference simple. Use a type parameter when one type can implement the trait several times for different types, as with From<T>.
Q10. What does #[derive] do, and what are its limits?
It asks the compiler to generate a standard implementation, field by field, for traits such as Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord and Default. Every field must implement the trait, and for generic types the derive adds a bound such as T: Clone. Traits whose output is a design choice, like Display, cannot be derived from the standard library.
Q11. What is a blanket implementation?
An implementation of a trait for every type satisfying a bound, such as impl<T: Display> ToString for T. It gives behaviour to whole families of types at once, including types defined later in other crates. Because of coherence, you cannot also write a conflicting specific implementation for a type covered by a blanket one.
Q12. Does Rust have inheritance through traits?
No. A supertrait such as trait Describe: Display only says that implementors must also implement Display. There is no field inheritance and no subclassing. Code reuse comes from default methods, generics, blanket implementations and composition.
Key takeaways
- A trait declares behaviour; types implement it in separate
impl Trait for Typeblocks, and default methods can build on required ones. - Bounds can be written as
impl Trait,<T: Trait>orwhere; all use static dispatch through monomorphisation. - Return-position
impl Traithides one concrete type; returning different types needsBox<dyn Trait>or a common type. dyn Traitis unsized and lives behind a fat pointer with a vtable; it allows mixed types at run time with an indirect call per method.- Only dyn compatible traits can be trait objects: no
Selfreturns and no generic methods. - The orphan rule requires the trait or the type to be local; wrap foreign types in a newtype.
- Derive
Debug,Clone,PartialEq,Eq,Hash,PartialOrd,OrdandDefaultwhere they fit; floats cannot beEqorHash. - Implement
Fromto getInto, implementDisplayto getto_string, and implementnextto get the wholeIteratortoolkit.
Next lesson
Continue with Lifetimes in Rust.

