RustTraits

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).

RUST
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:

RUST
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.

RUST
// 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 error
Warning
Violating the orphan rule causes a compile error: "only traits defined in the current crate can be implemented for types defined outside of the crate."
Default 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:

RUST
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:

RUST
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());
}
Tip
`impl Trait` in function parameters is syntactic sugar for a generic with that trait bound. The compiler creates a monomorphized copy for each concrete type at the call site.
Standard Library Traits

The standard library ships dozens of traits. Understanding the most common ones unlocks idiomatic Rust:

Trait

Purpose

Key Method

Display

Human-readable formatting (for end users)

fmt(&self, f: &mut Formatter)

Debug

Developer/debug formatting (for {:?})

fmt(&self, f: &mut Formatter)

Clone

Explicit deep copy of a value

clone(&self) -> Self

Copy

Implicit bitwise copy (no move semantics)

Marker — no methods

PartialEq

Equality comparison (==, !=)

eq(&self, other: &Self) -> bool

Eq

Total equality (PartialEq + reflexive guarantee)

Marker — no extra methods

PartialOrd

Partial ordering (<, >, <=, >=)

partial_cmp(&self, other: &Self)

Ord

Total ordering (all pairs comparable)

cmp(&self, other: &Self) -> Ordering

Iterator

Iteration protocol

next(&mut self) -> Option<Self::Item>

From

Infallible type conversion

from(value: T) -> Self

Into

Infallible conversion (auto-derived from From)

into(self) -> T

Default

Create a sensible default value

default() -> Self

Drop

Custom cleanup when value goes out of scope

drop(&mut self)

Send

Safe to transfer across threads

Marker — no methods

Sync

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:

RUST
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:

RUST
#[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, Default

  • All fields of the struct must also implement the derived trait

  • For non-derivable traits (like Display), you must write the implementation manually

  • Third-party crates like serde add 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):

RUST
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:

RUST
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 automatically
Send 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:

RUST
// 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);
}
Note
The `Send` and `Sync` constraints are checked at compile time. If you try to share a non-`Sync` type across threads, you get a compile error — not a runtime data race.
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:

RUST
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
}
Warning
`impl Trait` in return position requires all branches to return the **same** concrete type. To return different concrete types, use `Box<dyn Trait>` (dynamic dispatch) instead.
Summary

Concept

Syntax

Key Point

Define trait

trait T { fn method(&self); }

Lists required method signatures

Implement trait

impl Trait for Type { ... }

Orphan rule: own the trait or the type

Default method

Provide body in trait definition

Implementors can override or accept it

Trait parameter

fn f(x: &impl Trait)

Monomorphized at compile time

Derive

#[derive(Debug, Clone, ...)]

Auto-generates boilerplate impls

From/Into

impl From<A> for B

Into is auto-derived from From

Drop

impl Drop for T { fn drop }

Custom cleanup on scope exit

Send/Sync

Marker traits

Thread-safety enforced at compile time

Return trait

-> impl Trait

Hides concrete type; all branches must match