Skip to content

Traits vs interfaces

A trait is a set of methods that types can implement, like a Java interface with default methods, but without inheritance between types.

A Java interface declares methods. A class implements it, and since Java 8 the interface can have default methods. For shared fields, Java uses an abstract base class.

Java

interface PriceRule {
String name();
long apply(long cents);
default String describe(long cents) {
return name() + ": " + cents + " -> " + apply(cents);
}
}
record PercentOff(long percent) implements PriceRule {
public String name() {
return "percent off";
}
public long apply(long cents) {
return cents - cents * percent / 100;
}
}

Rust

pub trait PriceRule {
fn name(&self) -> &str;
fn apply(&self, cents: u64) -> u64;
/// A default method, like a Java `default` method in an interface.
fn describe(&self, cents: u64) -> String {
format!("{}: {} -> {}", self.name(), cents, self.apply(cents))
}
}
pub struct PercentOff(pub u64);
impl PriceRule for PercentOff {
fn name(&self) -> &str {
"percent off"
}
fn apply(&self, cents: u64) -> u64 {
cents - cents * self.0 / 100
}
}
pub struct FreeShippingOver(pub u64);
impl PriceRule for FreeShippingOver {
fn name(&self) -> &str {
"free shipping"
}
fn apply(&self, cents: u64) -> u64 {
if cents >= self.0 { cents } else { cents + 495 }
}
}

The structure is the same as in Java: a trait declares methods, can give default bodies, and each type implements it in an impl Trait for Type block. The differences are in what else is possible, and in what is not:

  • No inheritance of data. A trait has no fields. There are no abstract classes and no extends.
  • Implement for any type. You can implement your trait for a type from another crate, even for u64 or Vec<T>.
  • Static dispatch by default. A generic function that takes a trait is compiled once for each type. Dynamic dispatch, as in Java, is opt-in with dyn.
  • Associated types and constants. A trait can declare a type (type Item;) or a constant (const MAX: u32;) that each implementation chooses.
pub trait PriceRule {
fn name(&self) -> &str;
fn apply(&self, cents: u64) -> u64;
/// A default method, like a Java `default` method in an interface.
fn describe(&self, cents: u64) -> String {
format!("{}: {} -> {}", self.name(), cents, self.apply(cents))
}
}
pub struct PercentOff(pub u64);
impl PriceRule for PercentOff {
fn name(&self) -> &str {
"percent off"
}
fn apply(&self, cents: u64) -> u64 {
cents - cents * self.0 / 100
}
}
pub struct FreeShippingOver(pub u64);
impl PriceRule for FreeShippingOver {
fn name(&self) -> &str {
"free shipping"
}
fn apply(&self, cents: u64) -> u64 {
if cents >= self.0 { cents } else { cents + 495 }
}
}
  1. Line 1: pub trait: the interface. Every method in it is public if the trait is public.
  2. Line 7: A default method. It can call the required methods, like a Java default method.
  3. Line 12: A tuple struct with one field, self.0. The struct does not say which traits it implements.
  4. Line 14: impl PriceRule for PercentOff is the implements clause, in its own block. It can be in another file of the crate.
  5. Line 26: A second implementation. describe comes from the default.

Implement a trait for a type you did not write

Section titled “Implement a trait for a type you did not write”

You can add methods to u64 by defining a trait and implementing it. This is like C# extension methods. The caller must import the trait with use to call its methods:

/// A trait of this crate, implemented for `u64`, a type from the standard library.
pub trait CentsExt {
fn as_euros(&self) -> String;
}
impl CentsExt for u64 {
fn as_euros(&self) -> String {
format!("€{}.{:02}", self / 100, self % 100)
}
}
pub fn label(price: u64) -> String {
price.as_euros() // reads like a method of `u64`
}

The orphan rule limits this: either the trait or the type must be from your crate. You cannot implement Display (from the standard library) for Vec<u64> (from the standard library). This rule stops two crates from giving conflicting implementations.

A function that takes “any PriceRule” can be written in two ways that mean the same:

/// Generic: the compiler makes a copy of this function for each rule type.
pub fn final_price<R: PriceRule>(rule: &R, cents: u64) -> u64 {
rule.apply(cents)
}
/// The same, written with `impl Trait` in argument position.
pub fn final_price_short(rule: &impl PriceRule, cents: u64) -> u64 {
rule.apply(cents)
}

The compiler makes a separate copy of the function for each concrete type that calls it (monomorphization, see Generics). Each call goes directly to the right apply, and the compiler can inline it. There is no vtable and no heap allocation.

When one list must hold different types, you need a trait object: &dyn PriceRule or Box<dyn PriceRule>. This is how all Java interface calls work: a pointer to the value plus a pointer to a table of methods (the vtable).

/// Trait objects: different rule types in one list. Each call goes through a vtable.
pub fn apply_all(rules: &[Box<dyn PriceRule>], cents: u64) -> u64 {
rules.iter().fold(cents, |price, rule| rule.apply(price))
}
pub fn checkout_rules() -> Vec<Box<dyn PriceRule>> {
vec![Box::new(PercentOff(10)), Box::new(FreeShippingOver(5_000))]
}
Rust Dispatch Java
fn f<T: Rule>(r: &T) or fn f(r: &impl Rule) static, one copy per type (no equivalent)
fn f(r: &dyn Rule) dynamic, through a vtable every interface call
Vec<Box<dyn Rule>> dynamic, each value on the heap List<Rule>
enum Rule { A, B } with match static, closed set of types sealed interface with switch

When you review, ask which one the code needs. A fixed, known set of types is often an enum (see Enums and pattern matching). One type per call is a generic. Only an open set of types in one collection needs dyn. See S22: Box<dyn Trait> where a generic or enum is sufficient.

Many “magic” methods in Java are traits in Rust. These are the ones you will meet in almost every codebase:

Trait What it gives Java
Debug {:?} formatting toString() for developers
Display {} formatting and to_string() toString() for users
Clone, Copy copies (see Clone and Copy) copy constructor
PartialEq, Eq, Hash ==, map keys equals, hashCode
PartialOrd, Ord <, sorting Comparable
Default Type::default() no-argument constructor
From, Into conversions that cannot fail constructors, valueOf
TryFrom, TryInto conversions that can fail valueOf that throws
AsRef, Borrow “can be viewed as a &T” (none)
Deref *x and auto-deref, for smart pointers (none)
Iterator, IntoIterator for loops, adapters Iterator, Iterable
Drop cleanup at end of scope AutoCloseable.close()
Send, Sync safe to move or share between threads (none)
Error an error type with an optional cause Throwable

Two of them, written by hand:

#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
pub struct Money {
cents: u64,
}
impl fmt::Display for Money {
// `toString()` for users: used by `format!("{}")` and `to_string()`
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "€{}.{:02}", self.cents / 100, self.cents % 100)
}
}
impl From<u64> for Money {
// a conversion that cannot fail: `Money::from(1999)` or `1999.into()`
fn from(cents: u64) -> Self {
Self { cents }
}
}

impl From<u64> for Money also gives you Into<Money> for u64 for free, so 1999.into() works where a Money is expected.

A Java base class gives shared fields and shared behavior. In Rust, shared fields go in a struct that each type has as a field. Shared behavior goes in a trait:

Java

abstract static class AuditedEntity {
protected final String createdBy;
AuditedEntity(String createdBy) {
this.createdBy = createdBy;
}
String createdBy() {
return createdBy;
}
}
static final class Invoice extends AuditedEntity {
final String number;
Invoice(String createdBy, String number) {
super(createdBy);
this.number = number;
}
}

Rust

pub struct AuditInfo {
pub created_by: String,
pub version: u32,
}
/// Java: `class Invoice extends AuditedEntity`. Rust: `Invoice` has an `AuditInfo`.
pub struct Invoice {
pub audit: AuditInfo,
pub number: String,
}
pub struct Refund {
pub audit: AuditInfo,
pub invoice_number: String,
}
/// Shared behavior goes in a trait that both types implement.
pub trait Audited {
fn audit(&self) -> &AuditInfo;
fn created_by(&self) -> &str {
&self.audit().created_by
}
}
impl Audited for Invoice {
fn audit(&self) -> &AuditInfo {
&self.audit
}
}
impl Audited for Refund {
fn audit(&self) -> &AuditInfo {
&self.audit
}
}

The trait has one required method (audit) that gives access to the shared data, and default methods built on it. This is a common pattern. It is more explicit than inheritance: each type says what it shares.

  • Small traits. One to five methods, for one capability: Display, PriceRule, Payable.
  • A trait has a reason to exist: two or more implementations, a test double, or an extension point for other crates.
  • Generics or impl Trait by default, dyn Trait where a collection holds different types, and enum where the set of types is closed.
  • Standard traits instead of custom methods. Display instead of fn to_text(), From instead of fn from_u64(), Default instead of fn empty().
  • Derive what you can. #[derive(Debug, Clone, PartialEq)] on most data types.
  • Deep trait hierarchies that copy a Java class hierarchy: trait Document: AuditedEntity: Entity. See S13: Trait hierarchy that copies a Java class hierarchy.
  • Deref to a “base” struct, to get inherited-looking methods. See S14: Deref used to imitate inheritance.
  • Box<dyn Trait> everywhere, also where a generic or an enum works. See S22: Box<dyn Trait> where a generic or enum is sufficient.
  • A trait with exactly one implementation, named like a Java service (OrderService and OrderServiceImpl), with no tests that use another implementation.
  • as_any() and downcasting to imitate instanceof. If the code must know the concrete type, the trait is the wrong abstraction. An enum is often the fix.
  • Custom to_string(), equals() or clone_order() methods instead of the standard traits.

The task: an order store that a report function reads.

  1. WorseAn OrderService trait with one OrderServiceImpl, and a &dyn parameter. A Java interface-per-class habit.

    pub trait OrderService {
    fn find(&self, id: u64) -> Option<&Order>;
    fn total_cents(&self) -> u64;
    }
    pub struct OrderServiceImpl {
    orders: Vec<Order>,
    }
    impl OrderServiceImpl {
    pub fn new(orders: Vec<Order>) -> Self {
    Self { orders }
    }
    }
    impl OrderService for OrderServiceImpl {
    fn find(&self, id: u64) -> Option<&Order> {
    self.orders.iter().find(|o| o.id == id)
    }
    fn total_cents(&self) -> u64 {
    self.orders.iter().map(|o| o.total_cents).sum()
    }
    }
    pub fn report(service: &dyn OrderService) -> u64 {
    service.total_cents()
    }
    Run in Playground (opens the Rust Playground in a new tab)
  2. BetterA plain struct with methods. Add a trait when a second implementation exists.

    pub struct Orders {
    orders: Vec<Order>,
    }
    impl Orders {
    pub fn new(orders: Vec<Order>) -> Self {
    Self { orders }
    }
    pub fn find(&self, id: u64) -> Option<&Order> {
    self.orders.iter().find(|o| o.id == id)
    }
    pub fn total_cents(&self) -> u64 {
    self.orders.iter().map(|o| o.total_cents).sum()
    }
    }
    pub fn report(orders: &Orders) -> u64 {
    orders.total_cents()
    }
    Run in Playground (opens the Rust Playground in a new tab)

The “worse” version compiles and works. Its costs: twice the code, a dynamic call for no reason, and a reader who looks for the second implementation. In Java, the interface also exists for mocking frameworks and dependency injection containers. In Rust, tests usually build real values, and a trait can be added later without changing callers much.

The trait is correct when there are real alternatives: a database store and an in-memory store, or a test double that tests really use. Then a generic parameter (fn report(orders: &impl OrderStore)) is often better than &dyn.

  1. What is the difference between fn f(rule: &impl PriceRule) and fn f(rule: &dyn PriceRule)?

    Show the answer

    The first is static dispatch: the compiler makes a copy of f for each concrete type, and calls go directly to the method. The second is dynamic dispatch through a vtable, as in Java. Both accept any type that implements the trait.

  2. Can you implement your own trait for String?

    Show the answer

    Yes. Your trait is from your crate, so the orphan rule allows it. You cannot implement a standard library trait such as Display for String, because neither is yours.

  3. How does Rust share fields between types without inheritance?

    Show the answer

    Composition: put the shared fields in their own struct, and give each type a field of that struct. Shared behavior goes in a trait, often with default methods that use one accessor method.

  4. An agent wrote trait UserService and struct UserServiceImpl, with no other implementation. What do you ask?

    Show the answer

    What the trait is for. If no test or caller uses another implementation, remove the trait and keep the struct. A trait can be added later when it is needed.