Advanced Traits

Trait Design for Libraries

Designing public traits for a library requires balancing implementor ergonomics (how easy it is to implement the trait for a new type), caller ergonomics (how easy it is to call methods or use dyn Trait), and semver evolution (what changes can be made in minor releases without breaking downstream code).

Checklist of Library Trait Design Rules

Design Decision Recommendation Reason
Struct vs. impl bounds Put trait bounds on impl blocks and methods, not on the struct definition (unless required by Drop or associated types) Putting struct Cache<T: Clone> forces every struct mentioning Cache<T> to repeat T: Clone even for operations like .len()
Core vs. Extension split Keep required methods minimal in the base trait (Read, Iterator) and provide rich combinators as defaulted methods or an extension trait Implementors only write 1 core method (fn next) and inherit dozens of free methods
Reference & Box forwarding Implement your trait for &T, &mut T, and Box<T> (with T: ?Sized) Allows passing &mut impl MyTrait or Box<dyn MyTrait> to generic functions taking impl MyTrait by value
Dyn compatibility Add where Self: Sized to constructors and generic helper methods Preserves Box<dyn MyTrait> and &dyn MyTrait compatibility
Semver safety Always provide default bodies when adding new methods to an existing unsealed trait, or seal the trait initially Adding a required method without a default body breaks every downstream impl

Minimal Core Method + Rich Defaulted Methods

By designing a trait around a single primitive required method (fn write_byte(&mut self, b: u8)) and implementing higher-level operations (fn write_slice, fn write_u32_be) as default methods—which specific types can still override for bulk performance—implementors get maximum leverage from minimal code:

pub trait ByteWriter {
    // Single required method:
    fn write_byte(&mut self, byte: u8);

    // Defaulted bulk method (can be overridden for `memcpy` speed on contiguous buffers!):
    fn write_bytes(&mut self, bytes: &[u8]) {
        for &b in bytes {
            self.write_byte(b);
        }
    }

    // Defaulted big-endian integer helper:
    fn write_u32_be(&mut self, value: u32) {
        self.write_bytes(&value.to_be_bytes());
    }
}

// Forwarding impl so `&mut W` (including `&mut dyn ByteWriter`) satisfies `ByteWriter`:
impl<W: ByteWriter + ?Sized> ByteWriter for &mut W {
    fn write_byte(&mut self, byte: u8) {
        (**self).write_byte(byte);
    }

    fn write_bytes(&mut self, bytes: &[u8]) {
        (**self).write_bytes(bytes);
    }
}

struct ChecksumWriter {
    sum: u64,
    count: usize,
}

impl ByteWriter for ChecksumWriter {
    // Only needs to implement the single required method `write_byte`:
    fn write_byte(&mut self, byte: u8) {
        self.sum += u64::from(byte);
        self.count += 1;
    }
}

impl ByteWriter for Vec<u8> {
    fn write_byte(&mut self, byte: u8) {
        self.push(byte);
    }

    // Overrides `write_bytes` for contiguous slice extension:
    fn write_bytes(&mut self, bytes: &[u8]) {
        self.extend_from_slice(bytes);
    }
}

fn encode_header(mut out: impl ByteWriter) {
    out.write_bytes(b"MAGIC");
    out.write_u32_be(0x0102_0304);
}

fn main() {
    let mut buf = Vec::new();
    let mut checksum = ChecksumWriter { sum: 0, count: 0 };

    encode_header(&mut buf);
    encode_header(&mut checksum);

    println!("buf:      {:?}", buf);
    println!("checksum: count = {}, sum = {}", checksum.count, checksum.sum);
}
buf:      [77, 65, 71, 73, 67, 1, 2, 3, 4]
checksum: count = 9, sum = 368

Semver-Breaking vs. Non-Breaking Trait Changes

Change to a Public Trait Breaking Change? Mitigation
Adding a new method without a default body Major breaking change Provide a default implementation body or seal the trait
Adding a new method with a default body Minor (can cause method-call ambiguity only if a type already has a colliding method name) Safe in minor releases
Adding a new associated type Major breaking change (associated type defaults are unstable) Define a companion subtrait (TraitV2: Trait) instead
Tightening bounds on a trait or method Major breaking change Relaxing bounds is generally minor; tightening is breaking