Rust traits
last modified October 25, 2026
In this article we show how to define and use traits in Rust.
A trait defines a set of methods that a type must provide. It describes shared behaviour: different types can implement the same trait in their own way, and code written against the trait works with all of them. Traits are similar to interfaces in Java or C#.
Traits are central to Rust. Printing (Display,
Debug), comparison (PartialEq,
Ord), operators (Add), iteration
(Iterator), and conversions (From) are all
expressed as traits. Code that is generic over many types uses traits to say
what it needs from those types.
The examples were tested with Rust 1.97 and the 2024 edition. They are run as
Cargo projects with cargo run -q.
Defining and implementing a trait
A trait is defined with the trait keyword and implemented for a
type with an impl Trait for Type block.
trait Shape {
fn area(&self) -> f64;
fn name(&self) -> String;
}
struct Circle {
radius: f64,
}
struct Rectangle {
width: f64,
height: f64,
}
impl Shape for Circle {
fn area(&self) -> f64 {
std::f64::consts::PI * self.radius * self.radius
}
fn name(&self) -> String {
String::from("circle")
}
}
impl Shape for Rectangle {
fn area(&self) -> f64 {
self.width * self.height
}
fn name(&self) -> String {
String::from("rectangle")
}
}
fn main() {
let c = Circle { radius: 2.0 };
let r = Rectangle { width: 3.0, height: 5.0 };
println!("{} area: {:.2}", c.name(), c.area());
println!("{} area: {:.2}", r.name(), r.area());
}
We define a Shape trait and implement it for two structs.
trait Shape {
fn area(&self) -> f64;
fn name(&self) -> String;
}
The trait lists method signatures ending with a semicolon. These are the required methods; every implementing type must provide them.
impl Shape for Circle {
fn area(&self) -> f64 {
std::f64::consts::PI * self.radius * self.radius
}
fn name(&self) -> String {
String::from("circle")
}
}
The implementation must define all required methods with matching signatures. Leaving one out is a compile-time error.
$ cargo run -q circle area: 12.57 rectangle area: 15.00
Default methods
A trait can provide a default body for a method. Implementing types get the default for free and can override it.
trait Greeter {
fn name(&self) -> String;
fn greet(&self) -> String {
format!("Hello, {}!", self.name())
}
fn greet_loudly(&self) -> String {
self.greet().to_uppercase()
}
}
struct Person {
first: String,
}
struct Robot {
id: u32,
}
impl Greeter for Person {
fn name(&self) -> String {
self.first.clone()
}
}
impl Greeter for Robot {
fn name(&self) -> String {
format!("unit {}", self.id)
}
fn greet(&self) -> String {
format!("Greetings. I am {}.", self.name())
}
}
fn main() {
let p = Person { first: String::from("Lucia") };
let r = Robot { id: 42 };
println!("{}", p.greet());
println!("{}", p.greet_loudly());
println!("{}", r.greet());
println!("{}", r.greet_loudly());
}
The Greeter trait has one required and two default methods.
fn greet(&self) -> String {
format!("Hello, {}!", self.name())
}
A default method can call other methods of the trait, including the required ones whose bodies it does not know.
impl Greeter for Person {
fn name(&self) -> String {
self.first.clone()
}
}
Person implements only the required name method
and uses both defaults.
fn greet(&self) -> String {
format!("Greetings. I am {}.", self.name())
}
Robot overrides greet. The default
greet_loudly then calls the overridden version.
$ cargo run -q Hello, Lucia! HELLO, LUCIA! Greetings. I am unit 42. GREETINGS. I AM UNIT 42.
Trait bounds
Functions can accept any type that implements a given trait. The requirement is called a trait bound.
use std::fmt::{Debug, Display};
trait Summary {
fn summarize(&self) -> String;
}
#[derive(Debug)]
struct Article {
title: String,
words: u32,
}
impl Summary for Article {
fn summarize(&self) -> String {
format!("{} ({} words)", self.title, self.words)
}
}
fn notify(item: &impl Summary) {
println!("new: {}", item.summarize());
}
fn notify_generic<T: Summary>(item: &T) {
println!("generic: {}", item.summarize());
}
fn report<T, U>(item: &T, tag: U)
where
T: Summary + Debug,
U: Display,
{
println!("[{tag}] {}", item.summarize());
println!("{:?}", item);
}
fn main() {
let a = Article { title: String::from("Rust traits"), words: 1800 };
notify(&a);
notify_generic(&a);
report(&a, "draft");
report(&a, 7);
}
The example shows three ways of writing trait bounds.
fn notify(item: &impl Summary) {
The impl Trait syntax in a parameter position is the shortest
form. The function accepts a reference to any type that implements
Summary.
fn notify_generic<T: Summary>(item: &T) {
The same function written with a generic type parameter. This form is needed when several parameters must have the same type, or when we want to refer to the type by name.
fn report<T, U>(item: &T, tag: U)
where
T: Summary + Debug,
U: Display,
Multiple bounds are combined with +. When the bounds get long,
a where clause keeps the signature readable. The
report function accepts any tag that implements
Display, so both a string and an integer work.
$ cargo run -q
new: Rust traits (1800 words)
generic: Rust traits (1800 words)
[draft] Rust traits (1800 words)
Article { title: "Rust traits", words: 1800 }
[7] Rust traits (1800 words)
Article { title: "Rust traits", words: 1800 }
Trait objects
A trait object, written dyn Trait, lets us store
values of different types that implement the same trait in one collection.
trait Shape {
fn area(&self) -> f64;
fn name(&self) -> &str;
}
struct Circle(f64);
struct Square(f64);
struct Triangle(f64, f64);
impl Shape for Circle {
fn area(&self) -> f64 { std::f64::consts::PI * self.0 * self.0 }
fn name(&self) -> &str { "circle" }
}
impl Shape for Square {
fn area(&self) -> f64 { self.0 * self.0 }
fn name(&self) -> &str { "square" }
}
impl Shape for Triangle {
fn area(&self) -> f64 { 0.5 * self.0 * self.1 }
fn name(&self) -> &str { "triangle" }
}
fn total_area(shapes: &[Box<dyn Shape>]) -> f64 {
shapes.iter().map(|s| s.area()).sum()
}
fn main() {
let shapes: Vec<Box<dyn Shape>> = vec![
Box::new(Circle(1.0)),
Box::new(Square(2.0)),
Box::new(Triangle(3.0, 4.0)),
Box::new(Circle(0.5)),
];
for s in &shapes {
println!("{:<9} {:>6.2}", s.name(), s.area());
}
println!("total: {:.2}", total_area(&shapes));
let largest = shapes
.iter()
.max_by(|a, b| a.area().total_cmp(&b.area()))
.unwrap();
println!("largest: {}", largest.name());
}
We store different shapes in a single vector.
let shapes: Vec<Box<dyn Shape>> = vec![
Box::new(Circle(1.0)),
Box::new(Square(2.0)),
Box::new(Triangle(3.0, 4.0)),
Box::new(Circle(0.5)),
];
The concrete types have different sizes, so they cannot be stored directly
in a vector. Each value is put in a Box and the vector stores
the boxes as Box<dyn Shape>. The concrete type is erased;
only the trait methods are available.
fn total_area(shapes: &[Box<dyn Shape>]) -> f64 {
shapes.iter().map(|s| s.area()).sum()
}
Calls on a trait object use dynamic dispatch: the method to call is looked up at runtime in a table of function pointers (vtable) stored next to the data pointer.
.max_by(|a, b| a.area().total_cmp(&b.area()))
Floating-point numbers do not implement Ord, so we compare the
areas with total_cmp.
$ cargo run -q circle 3.14 square 4.00 triangle 6.00 circle 0.79 total: 13.93 largest: triangle
Static and dynamic dispatch
Generic functions and trait objects both accept values of different types, but they work differently.
trait Speak {
fn speak(&self) -> String;
}
struct Dog;
struct Cat;
impl Speak for Dog {
fn speak(&self) -> String { String::from("woof") }
}
impl Speak for Cat {
fn speak(&self) -> String { String::from("meow") }
}
fn static_call<T: Speak>(animal: &T) -> String {
animal.speak()
}
fn dynamic_call(animal: &dyn Speak) -> String {
animal.speak()
}
fn main() {
println!("{}", static_call(&Dog));
println!("{}", static_call(&Cat));
let animals: [&dyn Speak; 3] = [&Dog, &Cat, &Dog];
let sounds: Vec<String> = animals.iter().map(|a| dynamic_call(*a)).collect();
println!("{}", sounds.join(" "));
println!("&Dog: {} bytes", std::mem::size_of::<&Dog>());
println!("&dyn Speak: {} bytes", std::mem::size_of::<&dyn Speak>());
}
We call the same trait method through a generic function and through a trait object.
fn static_call<T: Speak>(animal: &T) -> String {
animal.speak()
}
For a generic function, the compiler creates a separate copy for each
concrete type it is used with, here static_call::<Dog> and
static_call::<Cat>. The calls are resolved at compile time
and can be inlined. This is static dispatch.
fn dynamic_call(animal: &dyn Speak) -> String {
animal.speak()
}
There is only one copy of dynamic_call; the method is found
through the vtable at runtime. In exchange for a small runtime cost, we can
mix types in one array and the binary does not grow with each new type.
println!("&dyn Speak: {} bytes", std::mem::size_of::<&dyn Speak>());
A reference to a trait object is a fat pointer: it holds a pointer to the data and a pointer to the vtable, so it is twice the size of a normal reference on a 64-bit system.
$ cargo run -q woof meow woof meow woof &Dog: 8 bytes &dyn Speak: 16 bytes
Returning traits
A function can return a type that implements a trait without naming the type.
use std::fmt::Display;
fn evens(limit: u32) -> impl Iterator<Item = u32> {
(0..limit).filter(|n| n % 2 == 0)
}
fn make_adder(n: i32) -> impl Fn(i32) -> i32 {
move |x| x + n
}
fn label(flag: bool) -> Box<dyn Display> {
if flag {
Box::new("enabled")
} else {
Box::new(0)
}
}
fn main() {
let v: Vec<u32> = evens(12).collect();
println!("{:?}", v);
let add5 = make_adder(5);
println!("{}", add5(10));
println!("{}", label(true));
println!("{}", label(false));
}
The example returns an iterator, a closure, and a trait object.
fn evens(limit: u32) -> impl Iterator<Item = u32> {
(0..limit).filter(|n| n % 2 == 0)
}
The real return type is a Filter struct with a closure type
inside it, which cannot even be written down. impl Iterator
hides it; the caller only knows it gets an iterator over
u32.
fn make_adder(n: i32) -> impl Fn(i32) -> i32 {
move |x| x + n
}
Every closure has its own unique type, so returning closures requires
impl Fn.
fn label(flag: bool) -> Box<dyn Display> {
if flag {
Box::new("enabled")
} else {
Box::new(0)
}
}
impl Trait stands for one concrete type chosen by the function.
When different branches return different types, a &str and
an i32 here, we must return a trait object instead.
$ cargo run -q [0, 2, 4, 6, 8, 10] 15 enabled 0
Implementing standard traits
Implementing traits from the standard library integrates our types with the language: formatting, operators, and conversions.
use std::fmt;
use std::ops::Add;
#[derive(Debug, Clone, Copy, PartialEq)]
struct Vec2 {
x: f64,
y: f64,
}
impl fmt::Display for Vec2 {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "[{}, {}]", self.x, self.y)
}
}
impl Add for Vec2 {
type Output = Vec2;
fn add(self, other: Vec2) -> Vec2 {
Vec2 { x: self.x + other.x, y: self.y + other.y }
}
}
impl From<(f64, f64)> for Vec2 {
fn from((x, y): (f64, f64)) -> Self {
Vec2 { x, y }
}
}
fn main() {
let a = Vec2 { x: 1.5, y: 2.0 };
let b: Vec2 = (0.5, -4.0).into();
let c = Vec2::from((3.0, 3.0));
println!("a = {a}");
println!("a + b = {}", a + b);
println!("a + b + c = {}", a + b + c);
println!("{}", a + b == Vec2 { x: 2.0, y: -2.0 });
}
The Vec2 struct implements Display,
Add, and From.
impl Add for Vec2 {
type Output = Vec2;
fn add(self, other: Vec2) -> Vec2 {
Vec2 { x: self.x + other.x, y: self.y + other.y }
}
}
Operators in Rust are traits from std::ops. Implementing
Add enables the + operator. The
Output associated type defines the type of the result.
impl From<(f64, f64)> for Vec2 {
fn from((x, y): (f64, f64)) -> Self {
Vec2 { x, y }
}
}
Implementing From gives us Vec2::from, and the
standard library automatically provides the matching Into
implementation, so we can also write (0.5, -4.0).into().
$ cargo run -q a = [1.5, 2] a + b = [2, -2] a + b + c = [5, 1] true
Associated types and constants
Besides methods, a trait can declare types and constants that each implementation defines.
struct Countdown {
current: u32,
}
impl Iterator for Countdown {
type Item = u32;
fn next(&mut self) -> Option<Self::Item> {
if self.current == 0 {
None
} else {
self.current -= 1;
Some(self.current + 1)
}
}
}
trait Unit {
const SYMBOL: &'static str;
const FACTOR: f64;
fn to_base(value: f64) -> f64 {
value * Self::FACTOR
}
}
struct Kilometer;
struct Mile;
impl Unit for Kilometer {
const SYMBOL: &'static str = "km";
const FACTOR: f64 = 1000.0;
}
impl Unit for Mile {
const SYMBOL: &'static str = "mi";
const FACTOR: f64 = 1609.344;
}
fn main() {
let cd = Countdown { current: 5 };
let v: Vec<u32> = cd.collect();
println!("{:?}", v);
let sum: u32 = Countdown { current: 10 }.filter(|n| n % 3 == 0).sum();
println!("sum: {sum}");
println!("3 {} = {} m", Kilometer::SYMBOL, Kilometer::to_base(3.0));
println!("3 {} = {} m", Mile::SYMBOL, Mile::to_base(3.0));
}
We implement the Iterator trait and define our own trait with
associated constants.
impl Iterator for Countdown {
type Item = u32;
fn next(&mut self) -> Option<Self::Item> {
The Iterator trait has an associated type Item and
one required method, next. Once next is
implemented, all the other iterator methods such as filter,
sum, and collect come as default methods.
trait Unit {
const SYMBOL: &'static str;
const FACTOR: f64;
fn to_base(value: f64) -> f64 {
value * Self::FACTOR
}
}
Associated constants are declared in the trait and given values in each
implementation. The to_base function has no
self parameter; it is called on the type as
Mile::to_base(3.0).
$ cargo run -q [5, 4, 3, 2, 1] sum: 18 3 km = 3000 m 3 mi = 4828.032 m
Supertraits
A trait can require that implementing types also implement other traits. These are called supertraits.
use std::fmt::Display;
trait Named {
fn name(&self) -> String;
}
trait Employee: Named + Display {
fn salary(&self) -> u32;
fn card(&self) -> String {
format!("{} | {} | {}", self.name(), self, self.salary())
}
}
struct Developer {
name: String,
lang: String,
}
impl Named for Developer {
fn name(&self) -> String {
self.name.clone()
}
}
impl Display for Developer {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "{} developer", self.lang)
}
}
impl Employee for Developer {
fn salary(&self) -> u32 {
4500
}
}
fn main() {
let d = Developer { name: String::from("Tomas"), lang: String::from("Rust") };
println!("{}", d.card());
}
The Employee trait builds on Named and
Display.
trait Employee: Named + Display {
fn salary(&self) -> u32;
fn card(&self) -> String {
format!("{} | {} | {}", self.name(), self, self.salary())
}
}
Because every Employee is guaranteed to be Named
and Display, the default card method can call
name and format self with {}. A type
that implements Employee without the supertraits does not
compile.
$ cargo run -q Tomas | Rust developer | 4500
Extending existing types
We can implement our own traits for types we did not define, including types from the standard library.
trait StrExt {
fn word_count(&self) -> usize;
fn is_palindrome(&self) -> bool;
}
impl StrExt for str {
fn word_count(&self) -> usize {
self.split_whitespace().count()
}
fn is_palindrome(&self) -> bool {
let chars: Vec<char> = self
.chars()
.filter(|c| c.is_alphanumeric())
.map(|c| c.to_ascii_lowercase())
.collect();
chars.iter().eq(chars.iter().rev())
}
}
trait Double {
fn double(&self) -> Self;
}
impl Double for i32 {
fn double(&self) -> Self {
self * 2
}
}
fn main() {
let s = "an old falcon in the sky";
println!("words: {}", s.word_count());
for w in ["level", "Never odd or even", "falcon"] {
println!("{w}: {}", w.is_palindrome());
}
println!("{}", 21.double());
}
We add methods to str and i32.
impl StrExt for str {
After the implementation, every string slice has the
word_count and is_palindrome methods, as long as
the StrExt trait is in scope.
chars.iter().eq(chars.iter().rev())
A palindrome reads the same forwards and backwards; we compare the iterator with its reversed version.
The orphan rule limits this: an implementation is allowed only if
the trait or the type is defined in our crate. We can implement our
StrExt for str, or Display for our
own struct, but not Display for Vec<i32>.
$ cargo run -q words: 6 level: true Never odd or even: true falcon: false 42
Blanket implementations
A blanket implementation implements a trait for all types that satisfy a bound.
use std::fmt::Display;
trait Describe {
fn describe(&self) -> String;
}
impl<T: Display> Describe for T {
fn describe(&self) -> String {
let s = self.to_string();
format!("'{}' ({} chars)", s, s.chars().count())
}
}
fn main() {
println!("{}", 42.describe());
println!("{}", 2.75.describe());
println!("{}", "hello".describe());
println!("{}", 'x'.describe());
println!("{}", String::from("žltý kôň").describe());
}
We implement Describe for every type that implements
Display.
impl<T: Display> Describe for T {
Integers, floats, strings, and characters all get the
describe method with a single implementation. The standard
library uses the same technique: to_string is available on every
Display type through a blanket implementation of the
ToString trait.
format!("'{}' ({} chars)", s, s.chars().count())
We count characters with chars().count(), since
len returns the number of bytes; the accented letters of the
last string take two bytes each in UTF-8.
$ cargo run -q '42' (2 chars) '2.75' (4 chars) 'hello' (5 chars) 'x' (1 chars) 'žltý kôň' (8 chars)
Source
The Rust Programming Language: Traits
The Rust Programming Language: Using Trait Objects
In this article we have worked with traits in Rust. Their use with type parameters is covered in Rust generics.
Author
List all Rust tutorials.