Item 13: 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, which is an Iterator that knows how many things it is iterating over.1 This method 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 different 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 has to implement only 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 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>(self) -> Cloned<Self>
where
    T: 'a + Clone,
    Self: Sized + Iterator<Item = &'a T>,
{
    Cloned::new(self)
}

In other words, the cloned() method is available only 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 usually be safely added to a trait even after an initial version of the trait is released. An addition like this preserves backward compatibility (see Item 21) for users and implementors of the trait, as long as the new method name does not clash with the name of a method from some other trait that the type implements. 2

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

The is_empty() method is currently a nightly-only experimental function.

2

If the new method happens to match a method of the same name in the concrete type, then the concrete method—known as an inherent implementation—will be used ahead of the trait method. The trait method can be explicitly selected instead by casting: <Concrete as Trait>::method().