Traits in Rust
A trait defines shared behaviour that types can implement. If you have used interfaces in Java or Go, or type classes in Haskell, traits will feel familiar. The key difference is that Rust's trait system is deeply integrated with the borrow checker, generics, and the type system as a whole — making it one of the most expressive interface systems in any systems language.
Defining a Trait
A trait body lists method signatures. Types that implement the trait must provide a concrete body for each method (unless a default is given, covered below).
trait Summary {
fn summarize(&self) -> String;
}
trait Greet {
fn greeting(&self) -> String;
fn greet(&self) { // a default implementation
println!("{}", self.greeting());
}
}Implementing a Trait
Use impl TraitName for Type to provide the concrete methods. Every required method (those
without a default body) must be implemented:
struct Article {
title: String,
author: String,
content: String,
}
impl Summary for Article {
fn summarize(&self) -> String {
format!("{}, by {} — {:.50}...", self.title, self.author, self.content)
}
}
struct Tweet {
username: String,
content: String,
}
impl Summary for Tweet {
fn summarize(&self) -> String {
format!("{}: {}", self.username, self.content)
}
}
fn main() {
let article = Article {
title: String::from("Rust is Fast"),
author: String::from("Alice"),
content: String::from("Rust achieves C-level performance without a GC..."),
};
let tweet = Tweet {
username: String::from("bob"),
content: String::from("Loving the borrow checker today!"),
};
println!("{}", article.summarize());
println!("{}", tweet.summarize());
}The Orphan Rule
Rust enforces the orphan rule (also called the coherence rule): you can implement a trait for a type only if at least one of the following is true:
- The trait is defined in your crate, OR
- The type is defined in your crate.
You cannot implement someone else's trait on someone else's type. This prevents two crates from providing conflicting implementations for the same type.
// OK — Display is from std, but MyStruct is ours
use std::fmt;
struct MyStruct(i32);
impl fmt::Display for MyStruct {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "MyStruct({})", self.0)
}
}
// OK — MyTrait is ours, Vec is from std
trait MyTrait { fn hello(&self); }
impl MyTrait for Vec<i32> {
fn hello(&self) { println!("hello from vec!"); }
}
// NOT OK — both Display and String are from std
// impl fmt::Display for String { ... } // compile errorDefault Implementations
A trait can provide a default body for any of its methods. Implementors may override the default or accept it as-is. Default methods can also call other methods of the same trait — even required ones:
trait Summary {
fn author(&self) -> String;
// Default implementation calls the required method
fn summarize(&self) -> String {
format!("(Read more from {}...)", self.author())
}
}
struct Tweet {
username: String,
content: String,
}
// Only needs to implement the required method
impl Summary for Tweet {
fn author(&self) -> String {
self.username.clone()
}
// summarize() uses the default — no need to override it
}
fn main() {
let t = Tweet { username: String::from("alice"), content: String::new() };
println!("{}", t.summarize());
}Traits as Function Parameters
Use impl Trait syntax to accept any type that implements a trait, without naming the
concrete type:
fn notify(item: &impl Summary) {
println!("Breaking news! {}", item.summarize());
}
// Equivalent generic form (needed when two params must be the same type)
fn notify_generic<T: Summary>(item: &T) {
println!("Breaking news! {}", item.summarize());
}
// Multiple trait bounds
fn notify_display(item: &(impl Summary + std::fmt::Debug)) {
println!("{:?} says: {}", item, item.summarize());
}Standard Library Traits
The standard library ships dozens of traits. Understanding the most common ones unlocks idiomatic Rust:
Trait | Purpose | Key Method |
|---|---|---|
| Human-readable formatting (for end users) |
|
| Developer/debug formatting (for |
|
| Explicit deep copy of a value |
|
| Implicit bitwise copy (no move semantics) | Marker — no methods |
| Equality comparison ( |
|
| Total equality (PartialEq + reflexive guarantee) | Marker — no extra methods |
| Partial ordering ( |
|
| Total ordering (all pairs comparable) |
|
| Iteration protocol |
|
| Infallible type conversion |
|
| Infallible conversion (auto-derived from From) |
|
| Create a sensible default value |
|
| Custom cleanup when value goes out of scope |
|
| Safe to transfer across threads | Marker — no methods |
| Safe to share reference across threads | Marker — no methods |
Implementing Display for Custom Types
Display is the trait behind println!("{}", value). Implementing it is how you
control how your types look to end users:
use std::fmt;
struct Color {
red: u8,
green: u8,
blue: u8,
}
impl fmt::Display for Color {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "#{:02X}{:02X}{:02X}", self.red, self.green, self.blue)
}
}
impl fmt::Debug for Color {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.debug_struct("Color")
.field("red", &self.red)
.field("green", &self.green)
.field("blue", &self.blue)
.finish()
}
}
fn main() {
let c = Color { red: 255, green: 128, blue: 0 };
println!("{}", c); // Display — user-friendly
println!("{:?}", c); // Debug — developer-friendly
}Deriving Traits with #[derive]
Many common traits have boilerplate implementations that the compiler can generate
automatically via the #[derive] attribute. This works for traits that have predictable,
field-by-field behaviour:
#[derive(Debug, Clone, PartialEq)]
struct Point {
x: f64,
y: f64,
}
fn main() {
let p1 = Point { x: 1.0, y: 2.0 };
let p2 = p1.clone(); // Clone
println!("{:?}", p1); // Debug
println!("Equal: {}", p1 == p2); // PartialEq
}Derivable traits include:
Debug,Clone,Copy,PartialEq,Eq,PartialOrd,Ord,Hash,DefaultAll fields of the struct must also implement the derived trait
For non-derivable traits (like
Display), you must write the implementation manuallyThird-party crates like
serdeadd their own derivable traits (Serialize,Deserialize)
From and Into
From and Into are the standard way to convert between types. Implementing From<T>
automatically gives you Into for free (the standard library provides a blanket impl):
struct Celsius(f64);
struct Fahrenheit(f64);
impl From<Celsius> for Fahrenheit {
fn from(c: Celsius) -> Self {
Fahrenheit(c.0 * 9.0 / 5.0 + 32.0)
}
}
fn main() {
let boiling = Celsius(100.0);
// Using From explicitly
let f = Fahrenheit::from(Celsius(100.0));
println!("Boiling: {}°F", f.0);
// Into is derived automatically
let f2: Fahrenheit = boiling.into();
println!("Boiling: {}°F", f2.0);
}The Drop Trait
Drop lets you run custom code when a value goes out of scope. It is Rust's equivalent of
a destructor. The compiler calls drop automatically — you never call it manually:
struct TempFile {
path: String,
}
impl Drop for TempFile {
fn drop(&mut self) {
println!("Cleaning up: {}", self.path);
// std::fs::remove_file(&self.path).ok();
}
}
fn main() {
let f = TempFile { path: String::from("/tmp/work.txt") };
println!("Using the file...");
} // f goes out of scope here — Drop::drop is called automaticallySend and Sync
Send and Sync are marker traits that the compiler uses to enforce thread safety.
You rarely implement them manually; the compiler derives them automatically based on a type's
fields:
// Send: safe to move to another thread
// Sync: safe to share (&T) across threads
// Most types are automatically Send + Sync
// Notable exceptions:
// - Rc<T> is NOT Send (use Arc<T> instead)
// - Cell<T> / RefCell<T> are NOT Sync (use Mutex<T> instead)
// - Raw pointers (*const T, *mut T) are neither
use std::sync::Arc;
use std::thread;
fn main() {
let data = Arc::new(vec![1, 2, 3]);
let data_clone = Arc::clone(&data);
let handle = thread::spawn(move || {
println!("From thread: {:?}", data_clone);
});
handle.join().unwrap();
println!("From main: {:?}", data);
}Traits as Return Types
You can return impl Trait from a function when the caller only needs to know that the
returned value implements a trait, not its concrete type. This is common with iterators and
closures:
fn make_summary(is_article: bool) -> impl Summary {
// Both branches must return the same concrete type
if is_article {
Article {
title: String::from("Generics"),
author: String::from("Alice"),
content: String::from("Content here"),
}
} else {
// compile error if you try to return Tweet here — different concrete type
Article { title: String::new(), author: String::new(), content: String::new() }
}
}
// Closures: return type is anonymous, impl Fn is the only option
fn make_adder(x: i32) -> impl Fn(i32) -> i32 {
move |y| x + y
}
fn main() {
let add5 = make_adder(5);
println!("{}", add5(3)); // 8
}Summary
Concept | Syntax | Key Point |
|---|---|---|
Define trait |
| Lists required method signatures |
Implement trait |
| Orphan rule: own the trait or the type |
Default method | Provide body in trait definition | Implementors can override or accept it |
Trait parameter |
| Monomorphized at compile time |
Derive |
| Auto-generates boilerplate impls |
From/Into |
| Into is auto-derived from From |
Drop |
| Custom cleanup on scope exit |
Send/Sync | Marker traits | Thread-safety enforced at compile time |
Return trait |
| Hides concrete type; all branches must match |