Traits

Default Implementations

A default implementation (also called a provided method) is a method inside a trait definition that includes a { ... } body rather than ending with a semicolon (;). Implementing types automatically inherit that behavior unless they explicitly provide their own overriding implementation.

Inheriting vs. Overriding a Default Method

When impl Trait for Type omits a method that has a default body, the type uses the trait's default body. When impl Trait for Type writes a new body for that method, the custom implementation replaces the default for that type:

trait HealthCheck {
    fn service_name(&self) -> &str;

    // Default implementation that calls the required `service_name` method:
    fn status_report(&self) -> String {
        format!("{}: OK (default check)", self.service_name())
    }
}

struct CacheNode {
    id: String,
}

// Inherits the default `status_report()`:
impl HealthCheck for CacheNode {
    fn service_name(&self) -> &str {
        &self.id
    }
}

struct DatabaseNode {
    id: String,
    active_connections: u32,
}

// Overrides `status_report()` with custom logic:
impl HealthCheck for DatabaseNode {
    fn service_name(&self) -> &str {
        &self.id
    }

    fn status_report(&self) -> String {
        format!(
            "{}: OK ({} active connections)",
            self.service_name(),
            self.active_connections
        )
    }
}

fn main() {
    let cache = CacheNode {
        id: String::from("redis-01"),
    };
    let db = DatabaseNode {
        id: String::from("postgres-primary"),
        active_connections: 42,
    };

    println!("{}", cache.status_report());
    println!("{}", db.status_report());
}
redis-01: OK (default check)
postgres-primary: OK (42 active connections)

Building Rich Trait APIs from a Single Required Method

Default methods can call other methods on Self declared in the same trait (even if those methods have no default implementation of their own). The standard library Iterator trait uses this pattern: implementing only fn next(&mut self) -> Option<Self::Item> automatically provides over 70 default methods (count, nth, map, filter, fold, collect, etc.).

Required by Implementor             Provided Automatically by Default Methods
+-----------------------------+     +------------------------------------------+
| fn next(&mut self)          | --> | fn count(self) -> usize                  |
|   -> Option<Self::Item>;    | --> | fn last(self) -> Option<Self::Item>      |
|                             | --> | fn nth(&mut self, n: usize) -> ...       |
+-----------------------------+     +------------------------------------------+

Here is a custom trait where implementing a single read_byte(&mut self) method unlocks higher-level reading methods automatically:

trait ByteSource {
    // Only required method:
    fn next_byte(&mut self) -> Option<u8>;

    // Provided methods built on top of `next_byte`:
    fn read_exact_vec(&mut self, count: usize) -> Option<Vec<u8>> {
        let mut buf = Vec::with_capacity(count);
        for _ in 0..count {
            buf.push(self.next_byte()?);
        }
        Some(buf)
    }

    fn count_remaining(&mut self) -> usize {
        let mut total = 0;
        while self.next_byte().is_some() {
            total += 1;
        }
        total
    }
}

struct SliceReader<'a> {
    data: &'a [u8],
    pos: usize,
}

impl<'a> ByteSource for SliceReader<'a> {
    fn next_byte(&mut self) -> Option<u8> {
        let b = *self.data.get(self.pos)?;
        self.pos += 1;
        Some(b)
    }
}

fn main() {
    let mut reader = SliceReader {
        data: &[10, 20, 30, 40, 50],
        pos: 0,
    };

    println!("First 3 bytes: {:?}", reader.read_exact_vec(3));
    println!("Remaining count: {}", reader.count_remaining());
}
First 3 bytes: Some([10, 20, 30])
Remaining count: 2

Overriding Default Methods for Performance Specialization

Even when a default method produces the right answer, a concrete type often knows a faster way to compute it. For example, counting elements by calling next_byte() one by one is $O(n)$, whereas SliceReader already knows self.data.len() - self.pos in $O(1)$ time. Overriding count_remaining replaces the $O(n)$ loop with an $O(1)$ slice calculation for SliceReader.

Calling Default Implementations from an Override Is Not Possible

In Rust, once an impl Trait for Type overrides a default method, the original default body from the trait cannot be called from inside that override (there is no super.method() syntax). If both the default behavior and custom overrides need to share common logic, extract that shared logic into a standalone helper function or a separate trait method.