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).
The is_empty()
method is currently a nightly-only experimental function.
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()
.