Item 12: Use default implementations to minimize required trait methods

The designer of a trait has two different audiences to consider: the programmers who will be implementing the trait, and those who will be using the trait. These two audiences lead to a degree of tension in the trait design:

  • To make the implementor's life easier, it's better for a trait to have the absolute minimum number of methods to achieve its purpose.
  • To make the user's life more convenient, it's helpful to provide a range of variant methods that cover all of the common ways that the trait might be used.

This tension can be balanced by including the wider range of methods that makes the user's life easier, but with default implementations provided for any methods that can be built from other, more primitive, operations on the interface.

A simple example of this is the is_empty() method for an ExactSizeIterator; it has a default implementation that relies on the len() trait method:

    fn is_empty(&self) -> bool {
        self.len() == 0
    }

The existence of a default implementation is just that: a default. If an implementation of the trait has a more optimal way of determining whether the iterator is empty, it can replace the default is_empty() with its own.

This approach leads to trait definitions that have a small number of required methods, plus a much larger number of default-implemented methods. An implementor for the trait only has to implement the former, and gets all of the latter for free.

It's also an approach that is widely followed by the Rust standard library; perhaps the best example there is the Iterator trait, which has a single required method (next) but which includes a panoply of pre-provided methods (Item 9), over 50 at the time of writing.

Trait methods can impose trait bounds, indicating that a method is only available if the types involved implement particular traits. The Iterator trait also shows that this is useful in combination with default method implementations. For example, the cloned() iterator method has a trait bound and a default implementation:

    fn cloned<'a, T: 'a>(self) -> Cloned<Self>
    where
        Self: Sized + Iterator<Item = &'a T>,
        T: Clone,
    {
        Cloned::new(self)
    }

In other words, the cloned() method is only available if the underlying Item type implements Clone; when it does, the implementation is automatically available.

The final observation about trait methods with default implementations is that new ones can be safely added to a trait even after an initial version of the trait is released. An addition like this preserves backwards compatibility (see Item 20) for both users and implementors1 of the trait.

So follow the example of the standard library and provide a minimal API surface for implementors, but a convenient and comprehensive API for users, by adding methods with default implementations (and trait bounds as appropriate).


1: This is true even if an implementor was unlucky enough to have already added a method of the same name in the concrete type, as the concrete method – known as an inherent implementation – will be used ahead of trait method. The trait method can be explicitly selected instead by casting: <Concrete as Trait>::method().