Traits
Traits, similar to interfaces in other programming languages, allow you to describe (and group) functionality in a general and reusable way. To do this you implement one or more trait for a given type which allows for the same basic behaviors across all types that implement that trait. Trait implementations inform the compiler of what functionality is available to that type’s instances. In this way, traits (along with generics) are tools to achieve polymorphism within Rust. A refresher in OOP reveals that polymorphism is a concept that allows you to treat different types similarly through the use of late or dynamic binding. In other words, it allows you to perform method m() on both type A and type B with similar results, even if method m() is implemented slightly differently for type A and type B. Traits can only be implemented once per type which allows the compiler to know exactly which types are involved and reduces the need for type annotations in calling code. Traits can also be used on their own as “trait objects” in a process called “dynamic dispatch” as a kind of generic specification. Dynamic dispatch is the process of selecting which implementation of a polymorphic operation to call at runtime.
Traits may contain abstract (unimplemented) and concrete (fully implemented) functions or types. All abstract functions and types on a trait must be defined concretely during trait implementation. Abstract trait functions defined on a trait, known as “required methods,” require code that may differ between implementations. The concrete methods on a trait apply to all types that implement the trait. These methods may be referred to as “default” or “provided” methods. Provided methods can be overridden for a particular trait implementation, but don’t have to be and often aren’t. Good design and implementation allows you to call the same method with the same basic results for every type that implements the same trait. For example, the standard library allows you to call iter()
on vectors, arrays, slices, Strings, etc. Even though they have different implementations, they all result in producing an iterator that yields immutable elements of the collection.
Traits can be declared, implemented, and called from different modules. If a trait is defined in a different module than where it’s implemented or used, it needs to be declared with a pub
keyword and may need to be brought into scope for use with a use
statement.
Traits can be implemented for any type, whether the type is a custom local type or defined in an external library. There are limitations though. The “orphan rule of coherence” dictates that when implementing traits for externally defined types, either the trait or the type (but not both) must be local to your crate. This means that you can implement a trait like std::fmt::Display
on a local (custom) type or define a custom trait for an external (abstract) type such as std::vec::Vec<T>
, but you cannot implement Display
for Vec
. This rule ensures that external code cannot break your code, and vice versa. You can get around this by writing a wrapper around a type using a tuple struct using the newtype pattern. See the newtype pattern subsection in the Trait Implementation section for an illustration. To maintain organization and coherence, it may be best to keep custom trait definitions and implementations near their associated type definitions.
Some traits simply indicate that a particular type can be used in some way. A marker trait in Rust is a trait that does not contain any methods or associated items. Its primary purpose is to convey information or metadata about types that implement the trait. Marker traits are often used to signify certain properties or capabilities of a type, which can then be leveraged by other parts of the code or the compiler for various purposes. Rust even has a dedicated standard library module called std::marker
that contains some (but not all) common marker traits.
Trait Declaration
To create a trait you must declare it with the trait
keyword followed by an identifier (name). Trait identifiers are capitalized by convention. Trait declarations may include a list of methods and/or types. The methods can be abstract, meaning they only contain headers/signatures, or you can declare default methods that contain fully defined methods. All abstract methods must be fully defined during trait implementation. For this reason abstract trait methods are referred to as “required methods”.
This example (stolen from the Rust book) illustrates a trait called Metadata
that will be implemented later. Metadata
has two methods called summarize()
and default()
. The abstract (undefined) method summarize()
is considered a required method for all implementations. This means that any type that implements Metadata
must also implement the summarize()
method. Notice that this method uses a terminal ;
character instead of curly braces {}
. The concrete (fully defined) method default()
is avaialable to any type that implements Metadata
. For this example the default()
method doesn’t really do anything. The example declares Metadata
as public with the pub
keyword, making it usable outside the local module.
pub trait Metadata { fn summarize(&self) -> String; // Required method with ; instead of body in {} fn default(&self) -> String { // Provided methods are fully defined String::from("(Placeholder text)") }}
Method chaining
Because all required methods must be defined concretely during implementation, it is possible to reference abstract methods from default methods in the trait declaration. For example, you could write some helper methods used in a more robust default method. The helper methods could be either concrete or abstract. This example declares an abstract author()
method and calls it from the default trait method. This allows you to fetch the same kind of information without explicitly naming it.
pub trait Metadata {
//Default method calls one other abstract trait method fn default(&self) -> String { format!("\n\tAuthor: {}\n\tTime: {}", &self.author(), time::static_time(8)) }
// Abstract trait method can be used by itself or simply with the default fn author(&self) -> String; fn summarize(&self) -> String;}
Associated types
Sometimes you need to write an abstract method for a trait that returns an abstract, unknown type. In these situations you can declare an associated type on a trait. Associated types in trait declarations are abstract types that, just like abstract methods, must be defined when you implement the trait. The Iterator
trait provides a good example of an associated type on a trait declaration. The Iterator
trait has one associated type named Item
. When you implement Iterator
on a type you must implement both the required method next()
and the associated type Item
. The next()
method takes a mutable reference to itself and returns the next Item
in the collection.
pub trait Iterator { type Item;
fn next(&mut self) -> Option<Self::Item>;
Why not use a generic type? The Iterator
trait could have been written something like the following.
pub trait Iterator<T> { fn next(&mut self) -> Option<T>;
This represents a design decision by the Rust team. Using generics would allow you to implement the trait multiple times. Because the compiler wouldn’t know which implementation you need for a given type any calling code would require a type annotation. Traits (and their associated types) can only be implemented once per type allowing you to avoid busy type annotations in your calling code.
Default type parameters
It is possible to name default types for generics in trait definitions. This can be helpful in operator overloading, or rather operator operation overloading. This example examines Rust’s std::ops::Add
trait. The right-hand side (Rhs) generic is given a default of Self
which means that if you don’t provide a concrete type for the implementation it defaults to the type that the trait is being implemented on. The Add
trait contains a default generic type in its declaration signature
pub trait Add<Rhs = Self> { type Output;
fn add(self, rhs: Rhs) -> Self::Output;}
// Uses the default Rhs type as Self which implements Point + Pointimpl Add for Point { ...}
// Specifies Meters as an Rhs override which implements Millimeters + Metersimpl Add<Meters> for Millimeters { ...}
Trait Implementation
To implement traits you need an impl
block similar to any other associated function/method implementation for a type. The biggest difference is that trait implementation blocks use the trait name and a for
keyword. This looks like impl TRAIT_NAME for TYPE
. The impl
block must contain full (concrete) definitions for any abstract methods or types contained in the trait definition whether they’re used in any calling code or not. Required method definitions must contain the same signature(s) as the trait’s original abstract method signature(s). If the trait and struct are declared in different crates, you would have to include the crate in a use
statement or use an absolute path to its declaration, e.g. impl crait::cncpt::traits::Metadata for NewsArticle
or impl Metadata for crate::cncpt::structs::NewsArticle
(or similar).
This example uses the same trait defined in the Complimentary trait methods subsection above. The example illustrates how this trait is implemented for two similar but different types. The major difference is that the second type Tweet
doesn’t have an author
field but instead has a username
field. This means that the traits abstract author()
method returns a username instead.
// Same method as defined in the trait declaration sectionpub trait Metadata {
//Default method calls one other abstract trait method fn default(&self) -> String { format!("\n\tAuthor: {}\n\tTime: {}", &self.author(), time::static_time(8)) }
// Two required methods fn author(&self) -> String; fn summarize(&self) -> String;}
// Example concrete type to implement the Metadata trait onpub struct NewsArticle { pub headline: String, pub location: String, pub author: String, pub content: String,}// Trait implementation block// Note that only generic trait methods are definedimpl Metadata for NewsArticle { fn author(&self) -> String { //The author is called "author" in the news article String::from(&self.author) } fn summarize(&self) -> String { format!("{}, by {} ({})", self.headline, self.author, self.location) }}
// Another concrete type to implement the Metadata trait onpub struct Tweet { pub username: String, pub content: String, pub reply: bool, pub retweet: bool,}// Trait implementation block// Note that only generic trait methods are definedimpl Metadata for Tweet { fn author(&self) -> String { //The author is called "username" in the tweet String::from(&self.username) } fn summarize(&self) -> String { format!("\n\t{}: {}", self.username, self.content) }}
Calling trait functions & methods
Calling trait functions works similarly to how you would call a type’s associated functions. You can call trait functions uses standard method call syntax. The only difference is that you may need to import the trait along with any types you want to use if your calling code exists in a separate module. This example imports and instantiates the Tweet
and NewsArticle
structs, as well as the Metadata
trait defined in the previous section. The code calls the summarize()
and default()
methods (respectively) as defined for the Metadata
trait.
// Imports the trait and the typesuse crate::cncpt::types::traits::{Metadata, NewsArticle, Tweet};
pub fn calling_code() { // Instantiates a news article struct let news_article = NewsArticle { headline: String::from("The best sandwich in Portland"), location: String::from("Oregon"), author: String::from("Peter Schmitz"), content: String::from("This is gonna be super long bro Im not entirely sure you're ready for this yet"), }; // Instantiates a tweet struct let tweet = Tweet { username: String::from("pschmitz"), content: String::from("This is a tweet so its gonna be a bit shorter than a news article. Its mostly jokes."), reply: false, retweet: true, }; // Calls the summarize trait method for the news article let news_summary = news_article.summarize(); // Calls the default trait method for the tweet let tweet = tweet.default(); println!("News article summary: {}\nTweet default: {}", news_summary, tweet);}
News article summary: The best sandwich in Portland, by Peter Schmitz (Oregon)Tweet default: (Placeholder text)
It may be desirable to create entire library crates just to define and implement traits on structures depending on how reusable/common the code is. This documentation simply illustrates easy onsey-twosies.
Nothing prevents you or anyone else from using the same name on a trait function/method as some other trait. For example, you could declare a trait named Wonky
and include a required method called into_iter()
. This is a super contrived example, but it’s helpful to illustrate fully qualified method syntax. This example creates a struct called IDK
which impleents both Wonky
and IntoIter
to show that any IDK
instance can access two different methods called into_iter()
from two different traits.
trait Wonky { fn into_iter(&self);}struct IDK { a: Vec<i32>}impl IDK { fn into_iter() { println!("Just another print statement") }}impl Wonky for IDK { fn into_iter(&self) { println!("Not your grandmothers iterator!"); }}impl IntoIterator for IDK { type Item = i32; type IntoIter = std::vec::IntoIter<Self::Item>;
fn into_iter(self) -> Self::IntoIter { self.a.into_iter() }}
pub fn calling_code() { let bare_bones = IDK { a: vec![4, 5, 6] }; // Default method syntax bare_bones.into_iter(); // Fully qualified syntax for inherent method // This does the same thing as the default syntax IDK::into_iter();
let vec = vec![1, 2, 3]; let object = IDK { a: vec, }; // Fully qualified syntax calls the IntoIterator trait method let iterator = IntoIterator::into_iter(object); for e in iterator { println!("{}", e) }
let another_vec = vec![1, 2, 3]; let another_object = IDK { a: another_vec, }; // Fully qualified syntax calls the Wonky trait method Wonky::into_iter(&another_object);}
Remember that associated functions that are not methods don’t have a self parameter. If you have more than one type or trait that defines associated functions (not methods) with the same name Rust cannot infer which one you’re calling. In these cases Rust needs fully qualified syntax to identify which function you want to call.
Consider this example lifted from the Rust book. Associated functions do not take a version of self so Rust cannot infer which function you’re calling without fully qualified syntax. The type annotation provides enough information to execute the desired function.
trait Animal { fn baby_name() -> String;}
struct Dog;
impl Dog { fn baby_name() -> String { String::from("Spot") }}
impl Animal for Dog { fn baby_name() -> String { String::from("puppy") }}
fn calling_code() { // Prints "A baby dog is called a Spot" because // Rust assumes you're calling the function on Dog println!("A baby dog is called a {}", Dog::baby_name());
// Results in a compilation error because there may // be more than one type that implement the Animal trait println!("A baby dog is called a {}", Animal::baby_name());
// Fully qualified syntax specifies the baby_name() function implemented for the Animal trait on Dog specifically println!("A baby dog is called a {}", <Dog as Animal>::baby_name());}
Fully qualified syntax with type annotation can be summed up with the following pseudocode. Note that there is only a receiver object if the function is a method. This syntax can be used anywhere, but Rust’s type inferance allows you to omit most of it.
<Type as Trait>::function_name(receiver_if_method, next_arg, ...);
The newtype pattern
To keep things sane Rust prevents you from implementing an externally defined trait on an externally defined type. This helps coherence in published libraries by preventing any potential conflicting implementations. You can get around this by creating a wrapper for a type in a tuple struct which effectively gives the type a new name and Rust is all, “New type, who dis?” Newtypes do not retain any of the functions or methods on the wrapped types. It is possible to use smart pointers to implement the Deref
trait on the newtype to gain access to all of the inner type’s functions/methods, or you could simply implement the desired functions manually effectively restricting the functionality for a given type.
use std::fmt;
struct Wrapper(Vec<String>);
// Uses the newtype Wrapper to implement Display for Vec<String>// two externally defined elements that would otherwise violate// the orphan rule// The implmentation works because Wrapper is a tuple struct// and Vec<T> is the item at index 0 in the tupleimpl fmt::Display for Wrapper { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "[{}]", self.0.join(", ")) }}
fn calling_code() { let w = Wrapper(vec![String::from("hello"), String::from("world")]); println!("w = {w}");}
Trait bounds
Sometimes you want to define a function that uses any type that implements one or more traits. It is not possible, nor is it practical, to name all the possible types that would be compatible. Instead Rust allows you to specify function parameters that require specific traits. This allows you to write functions that operate on all types that implement the named traits. Rust provides several syntax options for readability. This section covers the following syntax options.
- The trait bound syntax
- The impl trait syntax
where
clauses
For the most part, the trait bound and impl trait syntaxes are the most common or used in comparable situations. However, each of the syntax options defined in this section has benefits and drawbacks. Be sure to read through the options to figure out which syntax is best for what you’re trying to do.
Generic syntax
This syntax involves naming the type and its trait(s) after the function name and before the parameter list in angled brackets. This allows you to easily identify a single type that implements one or more traits. This syntax can be used with both types and functions.
// A function that accepts any type T that implements the std::fmt::Display traitfn print_type<T: Display>(value: T) { println!("{}", value);}
// A function that accepts any type T that implements our custom Metadata traitpub fn summary<T: Metadata>(item: &T) { println!("Summary: {}", item.summarize());}
// A struct with a generic type parameter// The struct can only be instantiated with types that implement Debugstruct Container<T: Debug> { item: T,}
// Implementing a trait for a generic type parameter with trait boundsimpl<T: Clone> Container<T> { // Methods here can use methods from the Clone trait on `self.item`}
The impl trait syntax
Alternatively, you could use the “impl Trait” syntax to express the same thing a little more elegantly.
pub fn impl_trait(item: &impl Metadata) { println!("More memes! {}", item.summarize());}
Multiple trait bounds
If you have multiple parameters with specific trait limitations things get a little tricky. For example, if you want to define a function to take multiple items of the same type you can use either the trait bound or impl trait syntax, but the trait bound syntax is clearer.
//Trait bound syntax is cleaner for defining parameters of the same typepub fn trait_bound<T: Metadata>(a: &T, b: &T) { println!("More memes! {}", a.summarize()); println!("Even MORE memes! {}", b.summarize());}//Impl trait syntax is repetitive for parameters of the same typepub fn impl_trait(a: &impl Metadata, b: &impl Metadata) { println!("Could be a Tweet or a NewsArticle! {}", a.summarize()); println!("The options are limitless! {}", b.default());}
However, if you want to define parameters that may be of any number of types that implement the same trait, or if you want to take completely different types that implement different traits, the impl trait syntax is the only option.
//Impl trait syntax is repetitive for parameters of the same typepub fn impl_trait(a: &impl Metadata, b: &impl Metadata) { println!("Could be a Tweet or a NewsArticle! {}", a.summarize()); println!("The options are limitless! {}", b.default());}
...
calling_code1(&tweet, &tweet); //Works just finecalling_code2(&tweet, &news_article); //Also works just fine!
Use the +
operator to name more than one trait per type. Again, the trait bound syntax only allows you to use parameters of the same type, whereas the impl trait syntax allows you to mix and match parameter types.
pub fn trait_bound<T: Metadata + Display>(a: &T, b: &T) { println!("More memes! {}", a.summarize()); println!("Even MORE memes! {}", b.summarize());}pub fn impl_trait(a: &(impl Metadata + Display), b: &(impl Metadata + Display)) { println!("Could be a Tweet or a NewsArticle! {}", a.summarize()); println!("The options are limitless! {}", b.default());}
The where
clause
The where
clause can be used as an elegant solution to function signatures that name lengthy lists of trait bounds of varying types. Consider the following example function with a lengthy signature.
fn long_list<T: Display + Clone, U: Clone + Debug>(t: &T, u: &U) -> i32 { ...}
To avoid lengthy lists of trait bounds that can be hard to read you can use the where
syntax for clarity. The where
keyword is still a part of the function signature, but can be broken up easier for readability. This example takes one long function signature and breaks it up with the where
clause for readability.
fn long_list<T, U>(t: &T, u: &U) -> i32where T: Display + Clone, U: Clone + Debug,{ ...}
Supertraits
You can use something like trait bounds to specify that a trait only takes types that implement another trait. For example, if you write a trait whose implementation also requires Display
you name it similar to the trait bounds syntax.
use std::fmt;
// OutlinePrint can only be implemented on types that// also implement Display// Display is a supertrait of OutlinePrinttrait OutlinePrint: fmt::Display { fn outline_print(&self) { let output = self.to_string(); let len = output.len(); println!("{}", "*".repeat(len + 4)); println!("*{}*", " ".repeat(len + 2)); println!("* {output} *"); println!("*{}*", " ".repeat(len + 2)); println!("{}", "*".repeat(len + 4)); }}
struct SuperPoint { x: i32, y: i32,}// Implements a custom traitimpl OutlinePrint for SuperPoint {}
// Implements the custom trait's supertraitimpl fmt::Display for SuperPoint { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "({}, {})", self.x, self.y) }}
pub fn calling_code_2() { let p = SuperPoint { x: 12, y: 23, }; p.outline_print();}
Trait Objects & Dynamic Dispatch
Trait objects are the closest things to “objects” in Rust because they combine data and behavior. However, they differ from other language’s concept of objects in that you cannot add data to a trait object. Indeed, they are less useful than objects in other languages; their purpose is specifically to allow abstraction across common behavior. This means you can define a trait that specifies a set of methods (the common behavior), and any type that implements this trait can be treated as an instance of the trait. This allows you to write code that operates on the trait rather than on the specific types, facilitating polymorphism and code reuse.
Trait objects allow you to specify a function parameter or field for any type that implements a specified trait. This process relies on dynamic dispatch, which determines at runtime which methods are available for the concrete type passed to the function. Dynamic dispatch contrasts with static dispatch, which resolves method calls for a given type at compile time. Trait objects can be used in place of either generic or concrete types.
Dynamic dispatch facilitates storing various types that implement the same trait in a single collection or passing them around as if they were of the same type. A generic type parameter can only be substituted with one concrete type at a time, whereas trait objects allow for multiple concrete types to fill in for the trait object at runtime. If you’ll only ever have homogeneous collections, using generics and trait bounds is preferable because the definitions will be monomorphized at compile time to use the concrete types.
A trait object points to both an instance of a type that implements the trait and a table used to look up trait methods on that type at runtime.
Trait objects are dynamically sized so you have to create them behind a reference with either &
or Box<T>
and using the dyn
keyword followed by the trait name. This example requires a referenced trait object because this situation also requires the object to implement Sized
, which is not specified.
// Brings the Display trait into scopeuse std::fmt::Display;
// Creates a trait object for any type that implements Displayfn print_message(item: &dyn Display) { println!("{}", item);}
// Trait definition with one required method draw()pub trait Draw { fn draw(&self);}
// A struct that has one field named components,// components is a vector of trait objects that implement the Draw traitpub struct Screen { pub components: Vec<Box<dyn Draw>>,}
// Implements Screen with one method named run which calls the required// draw() method on each of the vector's elements;// Trait objects all you to build components vectors out of more than one type such as Button, Box, Window, etc.impl Screen { pub fn run(&self) { for component in self.components.iter() { component.draw(); } }}
This concept—of being concerned only with the messages a value responds to rather than the value’s concrete type—is similar to the concept of duck typing in dynamically typed languages: if it walks like a duck and quacks like a duck, then it must be a duck!
If you want to be able to use a trait as a trait object, you need its methods to be object safe.
Multiple impl blocks
You can only implement a given trait once per type, but you can implement multiple traits per type. This allows you to specify implementations according to trait bounds. For example, the following code always implements the new()
method for the Pair<T>
type, but only implements the print_largest()
method on types that implement the Display
and PartialOrd
traits.
use std::fmt::Display;
struct Pair<T> { x: T, y: T,}
impl<T> Pair<T> { fn new(x: T, y: T) -> Self { Self { x, y } }}
impl<T: Display + PartialOrd> Pair<T> { fn print_largest(&self) { if self.x >= self.y { println!("The largest member is x = {}", self.x); } else { println!("The largest member is y = {}", self.y); } }}
Blanket Implementation
It is possible to implement traits for types that implement another trait. For example, the ToString
trait is implemented in the standard library for all types that implement Display
. This is called blanket implementation and is common in Rust. This means that you can call to_string()
on any type that implements the Display
trait, such as converting integers to strings.
impl<T: Display> ToString for T { // --snip--}
let s = 23.to_string();
Implementing Return Traits
It is also possible to return types that implement specific traits, or rather, one specific type that implements a trait per function. By doing this you can write a function that returns a type with a specific trait without naming the type. This works a bit like a constructor, but really just returns a type without naming the concrete type. This functionality is specifically useful for closures and iterators which may save the programmer from specifying unnecessarily long types.
fn return_type() -> impl Metadata { Tweet { username: String::from("horse_ebooks"), content: String::from( "of course, as you probably already know, people", ), reply: false, retweet: false, }}