Rust · lesson 14 of 30
Beginner
15- 1 Introduction to Rust
- 2 Setting Up Rust
- 3 Variables in Rust
- 4 Data Types in Rust
- 5 Operators in Rust
- 6 Control Flow in Rust
- 7 Functions in Rust
- 8 Ownership in Rust
- 9 Borrowing and References
- 11 Structs in Rust
- 12 Enums in Rust
- 13 Pattern Matching in Rust
- 17 Collections in Rust
- 25 Testing in Rust
- 26 File I/O in Rust
Intermediate
9Advanced
6Traits in Rust
Define and implement Rust traits: default methods, trait bounds, impl Trait vs dyn Trait, From/Into, operator overloading, supertraits, object safety.
By SkillByExample · Updated
Defining a Trait
Traits are Rust’s primary abstraction mechanism. A trait declares a set of method signatures that any conforming type must provide, letting you write code that works across many different types without knowing which specific type you’ll receive. This is how the standard library achieves polymorphism without inheritance — Display, Iterator, Clone, and hundreds of other behaviors are all traits.
trait Greet {
fn hello(&self) -> String;
// Default implementation — types can override this, but don't have to
fn goodbye(&self) -> String {
String::from("Goodbye!")
}
}
struct English;
struct Spanish;
impl Greet for English {
fn hello(&self) -> String {
String::from("Hello!")
}
}
impl Greet for Spanish {
fn hello(&self) -> String {
String::from("¡Hola!")
}
// Override the default goodbye
fn goodbye(&self) -> String {
String::from("¡Adiós!")
}
}
fn main() {
let e = English;
let s = Spanish;
println!("{}", e.hello());
println!("{}", e.goodbye()); // uses the default
println!("{}", s.hello());
println!("{}", s.goodbye()); // overridden
}
$ rustc traits_defining_a_trait.rs && ./traits_defining_a_trait
Hello!
Goodbye!
¡Hola!
¡Adiós!
Trait Bounds on Functions
Trait bounds let you write functions that accept any type implementing a given trait, rather than a single concrete type. This is how you get the benefits of generics while still constraining what operations are available. The compiler generates a specialized version of the function for each concrete type used — zero runtime overhead.
use std::fmt::Display;
// Syntax 1: inline bound — T must implement both PartialOrd and Display
fn print_largest<T: PartialOrd + Display>(list: &[T]) {
let mut largest = &list[0];
for item in list {
if item > largest {
largest = item;
}
}
println!("largest: {}", largest);
}
// Syntax 2: where clause — easier to read when bounds are complex
fn print_pair<T, U>(a: T, b: U)
where
T: Display + Clone,
U: Display,
{
println!("{} and {}", a.clone(), b);
}
fn main() {
print_largest(&[34, 50, 25, 100, 65]); // largest: 100
print_largest(&[1.5, 2.3, 0.8, 9.1]); // largest: 9.1
print_pair("hello", 42);
}
$ rustc traits_trait_bounds_on_functions.rs && ./traits_trait_bounds_on_functions
largest: 100
largest: 9.1
hello and 42
impl Trait Syntax — Static Dispatch
impl Trait in a parameter or return position is shorthand for a trait bound and uses static dispatch (monomorphization). In return position it is especially useful when you want to return a closure or an iterator without exposing the concrete type — the compiler knows the exact type, the caller only sees the trait.
use std::fmt::Display;
// Parameter position — equivalent to fn notify<T: Display>(item: &T)
fn notify(item: &impl Display) {
println!("Alert: {}", item);
}
// Return position — the concrete type is opaque; caller only sees Display
fn make_greeting(name: &str) -> impl Display {
format!("Hello, {}!", name)
}
fn main() {
notify(&42);
notify(&"hello");
println!("{}", make_greeting("Alice"));
}
$ rustc traits_impl_trait_syntax_static_dispatch.rs && ./traits_impl_trait_syntax_static_dispatch
Alert: 42
Alert: hello
Hello, Alice!
dyn Trait — Dynamic Dispatch
dyn Trait is a trait object — the concrete type is erased and method dispatch happens at runtime via a vtable (a table of function pointers). This costs a small indirection per call, but it enables heterogeneous collections where elements can be different types sharing a common interface. Use Box<dyn Trait> when you need to store or return values whose concrete type varies at runtime.
trait Animal {
fn sound(&self) -> &str;
fn name(&self) -> &str;
}
struct Dog;
struct Cat;
impl Animal for Dog {
fn sound(&self) -> &str { "woof" }
fn name(&self) -> &str { "dog" }
}
impl Animal for Cat {
fn sound(&self) -> &str { "meow" }
fn name(&self) -> &str { "cat" }
}
// Accepts a slice of any Animal — the types can differ
fn make_sounds(animals: &[Box<dyn Animal>]) {
for animal in animals {
println!("{} says {}", animal.name(), animal.sound());
}
}
fn main() {
// Dog and Cat can coexist in the same Vec thanks to dyn Trait
let animals: Vec<Box<dyn Animal>> = vec![
Box::new(Dog),
Box::new(Cat),
Box::new(Dog),
];
make_sounds(&animals);
}
$ rustc traits_dyn_trait_dynamic_dispatch.rs && ./traits_dyn_trait_dynamic_dispatch
dog says woof
cat says meow
dog says woof
Implementing Standard Library Traits
Display and Debug
Implementing standard library traits integrates your types into the broader Rust ecosystem. Display controls what println!("{}", ...) shows to users. Debug controls println!("{:?}", ...) for developer output. Implementing them means your type works naturally with format strings, error messages, and logging.
use std::fmt;
struct Temperature {
celsius: f64,
}
impl fmt::Display for Temperature {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
// User-facing: show both Celsius and the Fahrenheit conversion
write!(f, "{:.1}°C ({:.1}°F)", self.celsius, self.celsius * 1.8 + 32.0)
}
}
impl fmt::Debug for Temperature {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
// Developer-facing: show the raw struct fields
write!(f, "Temperature {{ celsius: {} }}", self.celsius)
}
}
fn main() {
let t = Temperature { celsius: 100.0 };
println!("{}", t);
println!("{:?}", t);
}
$ rustc traits_display_and_debug.rs && ./traits_display_and_debug
100.0°C (212.0°F)
Temperature { celsius: 100 }
Look closely at the Debug line: it prints celsius: 100, not 100.0. The hand-written Debug impl formats the f64 with plain {}, which drops the .0 from whole numbers; #[derive(Debug)] would have printed 100.0, and the Display impl gets 100.0 only because it asks for {:.1}.
From / Into
From and Into are conversion traits that express “this type can be constructed from another type.” They matter especially for error handling — the ? operator uses From to automatically convert between error types. Implementing From<T> gives you Into<T> for free through a blanket impl in the standard library.
#[derive(Debug)]
struct Celsius(f64);
#[derive(Debug)]
struct Fahrenheit(f64);
impl From<Celsius> for Fahrenheit {
fn from(c: Celsius) -> Fahrenheit {
Fahrenheit(c.0 * 1.8 + 32.0)
}
}
fn main() {
let boiling = Celsius(100.0);
let f: Fahrenheit = boiling.into(); // .into() uses the From impl above
println!("{:?}", f);
let freezing = Fahrenheit::from(Celsius(0.0));
println!("{:?} -> {} degrees", freezing, freezing.0);
}
$ rustc traits_from_into.rs && ./traits_from_into
Fahrenheit(212.0)
Fahrenheit(32.0) -> 32 degrees
Only From<Celsius> for Fahrenheit was written, yet boiling.into() works: the standard library’s blanket impl<T, U: From<T>> Into<U> for T supplies it. The type annotation on f is what tells into() which target to pick.
Iterator
Implementing Iterator on your type unlocks the entire iterator adapter ecosystem for free — map, filter, take, zip, enumerate, sum, collect, and 70+ more methods all work automatically once you define a single next method. This is the power of trait-based design: you implement the minimal interface and inherit a huge amount of behavior.
struct Counter {
count: u32,
max: u32,
}
impl Counter {
fn new(max: u32) -> Self { Self { count: 0, max } }
}
impl Iterator for Counter {
type Item = u32;
// The only method you must implement — everything else comes for free
fn next(&mut self) -> Option<u32> {
if self.count < self.max {
self.count += 1;
Some(self.count)
} else {
None
}
}
}
fn main() {
// sum(), zip(), skip() all work automatically from the Iterator impl
let sum: u32 = Counter::new(5).sum();
println!("sum 1..=5: {}", sum);
let pairs: Vec<_> = Counter::new(3)
.zip(Counter::new(3).skip(1))
.collect();
println!("{:?}", pairs);
}
$ rustc traits_iterator.rs && ./traits_iterator
sum 1..=5: 15
[(1, 2), (2, 3)]
Operator Overloading via Traits
Rust implements operator overloading through traits in the std::ops module. Overloading + for a custom type means implementing the Add trait. This keeps the language consistent — operators are just syntax sugar for trait method calls, so you can always fall back to the method if needed.
use std::ops::{Add, Mul, Neg};
#[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 Mul<f64> for Vec2 {
type Output = Vec2;
fn mul(self, scalar: f64) -> Vec2 {
Vec2 { x: self.x * scalar, y: self.y * scalar }
}
}
impl Neg for Vec2 {
type Output = Vec2;
fn neg(self) -> Vec2 { Vec2 { x: -self.x, y: -self.y } }
}
fn main() {
let a = Vec2 { x: 1.0, y: 2.0 };
let b = Vec2 { x: 3.0, y: 4.0 };
println!("{:?}", a + b);
println!("{:?}", a * 3.0);
println!("{:?}", -b);
println!("{:?}", a.add(b)); // the method behind the + operator
}
$ rustc traits_operator_overloading_via_traits.rs && ./traits_operator_overloading_via_traits
Vec2 { x: 4.0, y: 6.0 }
Vec2 { x: 3.0, y: 6.0 }
Vec2 { x: -3.0, y: -4.0 }
Vec2 { x: 4.0, y: 6.0 }
a.add(b) needs use std::ops::Add in scope — which is already there — and prints exactly what a + b printed. 3.0 * a would not compile: only Vec2 * f64 was implemented, and f64 * Vec2 is a separate impl Mul<Vec2> for f64.
Supertraits
Sometimes a trait only makes sense when another trait is already implemented. Supertraits express this dependency — they say “to implement this trait, you must also implement these other traits.” This is useful for building layered abstractions where a higher-level capability requires lower-level capabilities to already be in place.
use std::fmt;
// Summary requires Display and Debug — any type implementing Summary
// must also implement both formatting traits
trait Summary: fmt::Display + fmt::Debug {
fn summarize(&self) -> String;
// Default implementation can use the supertrait methods
fn print_summary(&self) {
println!("Summary: {}", self.summarize());
println!("Debug: {:?}", self);
}
}
#[derive(Debug)]
struct Article {
title: String,
words: usize,
}
impl fmt::Display for Article {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "\"{}\"", self.title)
}
}
impl Summary for Article {
fn summarize(&self) -> String {
format!("{} ({} words)", self, self.words)
}
}
fn main() {
let a = Article { title: String::from("Traits in Rust"), words: 1200 };
a.print_summary();
}
$ rustc traits_supertraits.rs && ./traits_supertraits
Summary: "Traits in Rust" (1200 words)
Debug: Article { title: "Traits in Rust", words: 1200 }
print_summary is a default method on Summary, yet it can use {:?} on self — the supertrait bound guarantees every implementor is Debug. Remove #[derive(Debug)] from Article and the impl Summary for Article line fails to compile, even though Article never calls Debug itself.
Object Safety
A trait is object-safe (usable as dyn Trait) when the compiler can build a vtable for it. The vtable is a fixed-size table of function pointers, so every method must be dispatchable without knowing the concrete type at compile time. Methods that return Self break this because the return type size would vary; methods with generic parameters break it because there would need to be infinitely many vtable entries.
trait MyClone {
fn my_clone(&self) -> Self;
}
struct Dot;
impl MyClone for Dot {
fn my_clone(&self) -> Self { Dot }
}
fn main() {
let items: Vec<Box<dyn MyClone>> = vec![Box::new(Dot)];
println!("{}", items.len());
}
$ rustc traits_object_safety.rs
error[E0038]: the trait `MyClone` is not dyn compatible
--> traits_object_safety.rs:12:28
|
12 | let items: Vec<Box<dyn MyClone>> = vec![Box::new(Dot)];
| ^^^^^^^ `MyClone` 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>
--> traits_object_safety.rs:2:27
|
1 | trait MyClone {
| ------- this trait is not dyn compatible...
2 | fn my_clone(&self) -> Self;
| ^^^^ ...because method `my_clone` references the `Self` type in its return type
= help: consider moving `my_clone` to another trait
= help: only type `Dot` implements `MyClone`; consider using it directly instead.
The compiler names the offending method and the reason. (Recent compilers say “dyn compatible” — the newer name for object safety.) A trait whose methods all take &self and return fixed-size types has no such problem:
trait Drawable {
fn draw(&self);
fn bounding_box(&self) -> (f64, f64, f64, f64);
}
struct Square(f64);
struct Dot;
impl Drawable for Square {
fn draw(&self) { println!("square with side {}", self.0); }
fn bounding_box(&self) -> (f64, f64, f64, f64) { (0.0, 0.0, self.0, self.0) }
}
impl Drawable for Dot {
fn draw(&self) { println!("dot"); }
fn bounding_box(&self) -> (f64, f64, f64, f64) { (0.0, 0.0, 0.0, 0.0) }
}
fn render(items: &[Box<dyn Drawable>]) {
for item in items {
item.draw();
println!(" bbox: {:?}", item.bounding_box());
}
}
fn main() {
render(&[Box::new(Square(2.0)), Box::new(Dot)]);
}
$ rustc traits_object_safety_2.rs && ./traits_object_safety_2
square with side 2
bbox: (0.0, 0.0, 2.0, 2.0)
dot
bbox: (0.0, 0.0, 0.0, 0.0)
Practice
-
Add a
Robot { id: u32 }type that implementsGreetwith onlyhello(returningformat!("BEEP. UNIT {} ONLINE.", self.id)). What doesgoodbye()return for it?Solution
BEEP. UNIT 7 ONLINE. Goodbye!Robotdidn’t writegoodbye, so it inherits the trait’s default implementation — the same oneEnglishuses. -
In the operator-overloading example, write
3.0 * ainstead ofa * 3.0. Then make it compile.Solution
error[E0277]: cannot multiply `{float}` by `Vec2` ... = help: the trait `Mul<Vec2>` is not implemented for `{float}`Operators dispatch on the left operand’s type. Add the mirror impl:
impl Mul<Vec2> for f64 { type Output = Vec2; fn mul(self, v: Vec2) -> Vec2 { v * self } }3.0 * athen printsVec2 { x: 3.0, y: 6.0 }. -
Remove
#[derive(Debug)]fromArticlein the supertraits example. Which line does the compiler blame first?Solution
error[E0277]: `Article` doesn't implement `Debug` --> ... | | impl Summary for Article { | ^^^^^^^ the trait `Debug` is not implemented for `Article`The error lands on the
impl Summaryline, not on any{:?}: the supertrait boundSummary: fmt::Debugis checked when you implementSummary. -
Implement
From<Celsius> for Kelvin(Kelvin(c.0 + 273.15)) and convertCelsius(25.0)with.into(). What prints, and what extra output doesrustcshow?Solution
It prints
Kelvin(298.15), butrustcfirst warnsfield \0` is never readforKelvin: a derivedDebugimpl doesn't count as reading a field. Printingk.0somewhere silences it — which is exactly why the lesson'sFrom/Intoexample printsfreezing.0`.