Traits
Introduction#
Traits are a way of describing a ‘contract’ that a struct
must implement. Traits typically define method signatures but can also provide implementations based on other methods of the trait, providing the trait bounds allow for this.
For those familiar with object oriented programming, traits can be thought of as interfaces with some subtle differences.
Syntax#
- trait Trait { fn method(…) -> ReturnType; … }
- trait Trait: Bound { fn method(…) -> ReturnType; … }
- impl Trait for Type { fn method(…) -> ReturnType { … } … }
- impl<T> Trait for T where T: Bounds { fn method(…) -> ReturnType { … } … }
Remarks#
- Traits are commonly likened to interfaces, but it is important to make a distinction between the two. In OO languages like Java, interfaces are an integral part of the classes that extend them. In Rust, the compiler knows nothing of a struct’s traits unless those traits are used.
Basics
Creating a Trait
trait Speak {
fn speak(&self) -> String;
}
Implementing a Trait
struct Person;
struct Dog;
impl Speak for Person {
fn speak(&self) -> String {
String::from("Hello.")
}
}
impl Speak for Dog {
fn speak(&self) -> String {
String::from("Woof.")
}
}
fn main() {
let person = Person {};
let dog = Dog {};
println!("The person says {}", person.speak());
println!("The dog says {}", dog.speak());
}
Static and Dynamic Dispatch
It is possible to create a function that accepts objects that implement a specific trait.
Static Dispatch
fn generic_speak<T: Speak>(speaker: &T) {
println!("{0}", speaker.speak());
}
fn main() {
let person = Person {};
let dog = Dog {};
generic_speak(&person);
generic_speak(&dog);
}
Static dispatch is used here, which means that Rust compiler will generate specialized versions of generic_speak
function for both Dog
and Person
types. This generation of specialized versions of a polymorphic function (or any polymorphic entity) during compilation is called Monomorphization.
Dynamic Dispatch
fn generic_speak(speaker: &Speak) {
println!("{0}", speaker.speak());
}
fn main() {
let person = Person {};
let dog = Dog {};
generic_speak(&person as &Speak);
generic_speak(&dog); // gets automatically coerced to &Speak
}
Here, only a single version of generic_speak
exists in the compiled binary, and the speak()
call is made using a vtable lookup at runtime. Thus, using dynamic dispatch results in faster compilation and smaller size of compiled binary, while being slightly slower at runtime.
Objects of type &Speak
or Box<Speak>
are called trait objects.
Associated Types
- Use associated type when there is a one-to-one relationship between the type implementing the trait and the associated type.
- It is sometimes also known as the output type, since this is an item given to a type when we apply a trait to it.
Creation
trait GetItems {
type First;
// ^~~~ defines an associated type.
type Last: ?Sized;
// ^~~~~~~~ associated types may be constrained by traits as well
fn first_item(&self) -> &Self::First;
// ^~~~~~~~~~~ use `Self::` to refer to the associated type
fn last_item(&self) -> &Self::Last;
// ^~~~~~~~~~ associated types can be used as function output...
fn set_first_item(&mut self, item: Self::First);
// ^~~~~~~~~~~ ... input, and anywhere.
}
Implemention
impl<T, U: ?Sized> GetItems for (T, U) {
type First = T;
type Last = U;
// ^~~ assign the associated types
fn first_item(&self) -> &Self::First { &self.0 }
fn last_item(&self) -> &Self::Last { &self.1 }
fn set_first_item(&mut self, item: Self::First) { self.0 = item; }
}
impl<T> GetItems for [T; 3] {
type First = T;
type Last = T;
fn first_item(&self) -> &T { &self[0] }
// ^ you could refer to the actual type instead of `Self::First`
fn last_item(&self) -> &T { &self[2] }
fn set_first_item(&mut self, item: T) { self[0] = item; }
}
Refering to associated types
If we are sure that a type T
implements GetItems
e.g. in generics, we could simply use T::First
to obtain the associated type.
fn get_first_and_last<T: GetItems>(obj: &T) -> (&T::First, &T::Last) {
// ^~~~~~~~ refer to an associated type
(obj.first_item(), obj.last_item())
}
Otherwise, you need to explicitly tell the compiler which trait the type is implementing
let array: [u32; 3] = [1, 2, 3];
let first: &<[u32; 3] as GetItems>::First = array.first_item();
// ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [u32; 3] may implement multiple traits which many
// of them provide the `First` associated type.
// thus the explicit "cast" is necessary here.
assert_eq!(*first, 1);
Constraining with associated types
fn clone_first_and_last<T: GetItems>(obj: &T) -> (T::First, T::Last)
where T::First: Clone, T::Last: Clone
// ^~~~~ use the `where` clause to constraint associated types by traits
{
(obj.first_item().clone(), obj.last_item().clone())
}
fn get_first_u32<T: GetItems<First=u32>>(obj: &T) -> u32 {
// ^~~~~~~~~~~ constraint associated types by equality
*obj.first_item()
}
Default methods
trait Speak {
fn speak(&self) -> String {
String::from("Hi.")
}
}
The method will be called by default except if it’s overwritten in the impl
block.
struct Human;
struct Cat;
impl Speak for Human {}
impl Speak for Cat {
fn speak(&self) -> String {
String::from("Meow.")
}
}
fn main() {
let human = Human {};
let cat = Cat {};
println!("The human says {}", human.speak());
println!("The cat says {}", cat.speak());
}
Output :
The human says Hi.
The cat says Meow.
Placing a bound on a trait
When defining a new trait it is possible to enforce that types wishing to implement this trait verify a number of constraints or bounds.
Taking an example from the standard library, the DerefMut
trait requires that a type first implement its sibling Deref
trait:
pub trait DerefMut: Deref {
fn deref_mut(&mut self) -> &mut Self::Target;
}
This, in turn, enables DerefMut
to use the associated type Target
defined by Deref
.
While the syntax might be reminiscent of inheritance:
- it brings in all the associated items (constants, types, functions, …) of the bound trait
- it enables polymorphism from
&DerefMut
to&Deref
This is different in nature:
- it is possible to use a lifetime (such as
'static
) as a bound - it is not possible to override the bound trait items (not even the functions)
Thus it is best to think of it as a separate concept.
Multiple bound object types
It’s also possible to add multiple object types to a Static Dispatch function.
fn mammal_speak<T: Person + Dog>(mammal: &T) {
println!("{0}", mammal.speak());
}
fn main() {
let person = Person {};
let dog = Dog {};
mammal_speak(&person);
mammal_speak(&dog);
}