In Chapter 10: Generics, we learned how to write code that can work with different types without having to duplicate the code for each type. Now, let's explore Traits, which define shared behavior that different types can implement.
Imagine you have several different types of vehicles: cars, trucks, and motorcycles. Each of these vehicles can move. Traits let you define what it means to "move" so that you can write code that works with any vehicle that can move, regardless of whether it's a car, truck, or motorcycle.
Central Use Case: You want to write a function that displays information about any item that can be "summarized." This could be a news article, a user profile, or a product description. Traits let you define what it means to be "summarizable" so that you can write one function that works for all these different types.
Key Concepts
Let's break down the key things to understand about traits:
Trait Definition: A trait is a collection of method signatures that define a set of behaviors. Think of it as a contract: if a type implements a trait, it promises to provide implementations for all the methods defined in the trait.
Implementing a Trait: You implement a trait for a specific type using the
implkeyword. This tells Rust that the type will provide concrete implementations for the trait's methods.Trait Bounds: You can use trait bounds to specify that a generic type must implement a particular trait. This allows you to write generic code that only works with types that have certain capabilities.
Default Implementations: Traits can provide default implementations for their methods. This allows types to implement only the methods that are specific to them, while relying on the default implementations for the rest.
Traits in Action: The Summarize Trait
Let's create a Summarize trait:
pub trait Summarize {
fn summarize(&self) -> String;
}
Explanation:
pub trait Summarize: This defines a public trait namedSummarize. Thepubkeyword makes the trait accessible from outside the current module.fn summarize(&self) -> String;: This defines a method signature namedsummarize.&self: This indicates that the method takes a reference to the type implementing the trait.-> String: This indicates that the method returns aString.
Now, let's create a struct and implement the Summarize trait for it:
struct NewsArticle {
headline: String,
author: String,
content: String,
}
impl Summarize for NewsArticle {
fn summarize(&self) -> String {
format!("{} by {}", self.headline, self.author)
}
}
Explanation:
struct NewsArticle: This defines a struct namedNewsArticlewith fields for the headline, author, and content.impl Summarize for NewsArticle: This tells Rust that we're implementing theSummarizetrait for theNewsArticlestruct.fn summarize(&self) -> String { ... }: This provides a concrete implementation for thesummarizemethod. In this case, it returns a string that combines the headline and author.
Now, let's use the Summarize trait:
fn main() {
let article = NewsArticle {
headline: String::from("Rust Traits are Awesome!"),
author: String::from("Jane Doe"),
content: String::from("This article explains Rust traits..."),
};
println!("Summary: {}", article.summarize());
}
Output:
Summary: Rust Traits are Awesome! by Jane Doe
Explanation:
We create an instance of the
NewsArticlestruct.We call the
summarizemethod on the instance. BecauseNewsArticleimplements theSummarizetrait, we can call the method just like any other method on the struct.
Traits in Action: Default Implementations
Let's add a default implementation to the Summarize trait:
pub trait Summarize {
fn summarize(&self) -> String {
String::from("(Read more...)")
}
}
Explanation:
- We add a default implementation for the
summarizemethod. This implementation simply returns the string "(Read more...)".
Now, let's create a struct and implement the Summarize trait for it, but without providing a custom implementation for the summarize method:
struct Tweet {
username: String,
content: String,
}
impl Summarize for Tweet {} // No custom summarize implementation!
Explanation:
- We implement the
Summarizetrait for theTweetstruct, but we don't provide any code within theimplblock. This means that theTweetstruct will use the default implementation of thesummarizemethod.
Now, let's use the Summarize trait:
fn main() {
let tweet = Tweet {
username: String::from("johndoe"),
content: String::from("Rust is great!"),
};
println!("Summary: {}", tweet.summarize());
}
Output:
Summary: (Read more...)
Explanation:
- We call the
summarizemethod on thetweetinstance. BecauseTweetuses the default implementation of thesummarizemethod, it prints "(Read more...)".
Traits as Parameters
You can use traits as parameters to functions. This lets you write functions that can accept any type that implements the trait.
pub fn display_summary(item: &impl Summarize) {
println!("Summary: {}", item.summarize());
}
Explanation:
pub fn display_summary(item: &impl Summarize): This defines a function nameddisplay_summarythat takes one argument nameditem.&impl Summarize: This indicates that theitemargument must be a reference to a type that implements theSummarizetrait.
Now, let's use this function:
fn main() {
let article = NewsArticle {
headline: String::from("Rust Traits are Awesome!"),
author: String::from("Jane Doe"),
content: String::from("This article explains Rust traits..."),
};
display_summary(&article);
let tweet = Tweet {
username: String::from("johndoe"),
content: String::from("Rust is great!"),
};
display_summary(&tweet);
}
Output:
Summary: Rust Traits are Awesome! by Jane Doe
Summary: (Read more...)
Explanation:
We call
display_summarywith both aNewsArticleand aTweet.The same
display_summaryfunction works for both types because both implement theSummarizetrait.
Internal Implementation (Simplified)
When you use traits, Rust uses a technique called dynamic dispatch (or "trait objects") in some cases. This means that the compiler doesn't know at compile time which concrete implementation of a trait method will be called. Instead, it figures it out at runtime.
Here's a simplified sequence diagram of how this works:
Explanation:
The
Mainfunction callsdisplay_summarywith aNewsArticleinstance.DisplaySummaryneeds to call thesummarizemethod. Since it only knows that the input implementsSummarizeinterface, it doesn't know what the concretesummarizemethod is. It uses dynamic dispatch to look up the correct implementation at runtime.Rust finds the
summarizeimplementation forNewsArticleand calls it.The
summarizemethod returns a string.The
DisplaySummaryfunction prints the summary.The same process is repeated for the
Tweetinstance, but this time Rust finds the default implementation of thesummarizemethod.
Important Details (Not Covered Here for Simplicity):
Trait Objects: Dynamic dispatch is often implemented using "trait objects," which are essentially pointers to both the data and a table of function pointers (the "vtable") that contains the correct method implementations for that data type.
Static Dispatch (Generics): When you use generics with trait bounds (e.g.,
fn display_summary<T: Summarize>(item: &T)), Rust can use static dispatch. This means that the compiler knows at compile time which implementation of the trait method to call, and it can generate more efficient code.
Conclusion
Traits are a powerful tool in Rust that allows you to define shared behavior that different types can implement. They promote code reuse, abstraction, and polymorphism. Understanding traits is crucial for writing flexible and maintainable Rust code.
In the next chapter, we'll explore Lifetimes, which help the compiler ensure that references are always valid.


