11.1 Traits: Defining Shared Behavior
A trait in Rust defines a set of methods that a type must implement to conform to a certain interface or contract. Traits are central to Rust’s abstraction capabilities, enabling polymorphism and code sharing. For C programmers, think of them as a more formalized and compile-time-checked version of using function pointers within structs to achieve polymorphism.
Key Concepts
- Definition: A
trait
block specifies method signatures that constitute a shared behavior. Optionally, it can also provide default implementations for some methods. - Implementation: Types opt into a trait’s behavior using an
impl Trait for Type
block, providing concrete implementations for the required methods, or relying on defaults if available. - Abstraction: Functions and data structures can operate on any type that implements a specific trait, using trait bounds.
- Polymorphism: Traits allow different types to be treated uniformly based on shared capabilities, similar to how interfaces or abstract classes work, but without inheritance hierarchies.
11.1.1 Declaring and Implementing Traits
A trait is declared with the trait
keyword, followed by its name and a block containing method signatures. These signatures define the methods that any type implementing the trait must provide.
Traits can also provide default implementations for methods, which an implementing type can use or overwrite by providing its own version.
Many trait methods take a special first parameter representing the instance the method is called on: self
, &self
, or &mut self
. Note that &self
is shorthand for self: &Self
, where Self
is a type alias for the type implementing the trait (e.g., Article
or Tweet
in the examples below).
#![allow(unused)] fn main() { trait Summary { // Method signature: requires implementing types to provide this method. fn summarize(&self) -> String; // Takes an immutable reference to the instance // A method with a default implementation. Optional for implementors. fn description(&self) -> String { String::from("(No description)") // Default implementation } } }
To implement this trait for a specific type, such as a struct, use an impl
block. Within this block, you provide the concrete implementations for the methods defined in the trait signature. If the trait provides default implementations, you can choose to override them or use the defaults by simply not providing an implementation for that specific method.
#![allow(unused)] fn main() { trait Summary { fn summarize(&self) -> String; fn description(&self) -> String { String::from("(No description)") } } struct Article { title: String, content: String, } // Implement the Summary trait for the Article struct impl Summary for Article { fn summarize(&self) -> String { // Provide a concrete implementation for summarize if self.content.len() > 50 { format!("{}...", &self.content[..50]) } else { self.content.clone() } } // We don't provide `description`, so the default implementation from the // trait definition is used for Article instances. } struct Tweet { username: String, text: String, } // Implement the Summary trait for the Tweet struct impl Summary for Tweet { fn summarize(&self) -> String { format!("@{}: {}", self.username, self.text) } // Override the default implementation for description fn description(&self) -> String { format!("Tweet by @{}", self.username) } } }
As shown above, Article
uses the default description
, while Tweet
overrides it. A single type can implement multiple different traits, allowing types to compose behaviors in a modular way. Each trait implementation typically resides in its own impl
block.
11.1.2 Using Traits as Parameters (Trait Bounds)
You can write functions that accept any type implementing a specific trait using trait bounds. This allows functions to operate on data generically, based on capabilities rather than concrete types. This is commonly done using generic type parameters (<T: Trait>
) or the impl Trait
syntax in argument position.
trait Summary { fn summarize(&self) -> String; fn description(&self) -> String { String::from("(No description)") } } struct Article { title: String, content: String, } impl Summary for Article { fn summarize(&self) -> String { if self.content.len() > 50 { format!("{}...", &self.content[..50]) } else { self.content.clone() } } } struct Tweet { username: String, text: String, } impl Summary for Tweet { fn summarize(&self) -> String { format!("@{}: {}", self.username, self.text) } fn description(&self) -> String { format!("Tweet by @{}", self.username) } } // Using generic type parameter 'T' with a trait bound 'Summary' fn print_summary<T: Summary>(item: &T) { println!("Summary: {}", item.summarize()); println!("Description: {}", item.description()); } // Using 'impl Trait' syntax (often more concise for simple cases) fn notify(item: &impl Summary) { println!("Notification! {}", item.summarize()); } fn main() { let article = Article { title: String::from("Rust Traits"), content: String::from("Traits define shared behavior across different ..."), }; let tweet = Tweet { username: String::from("rustlang"), text: String::from("Check out the new release!"), }; print_summary(&article); // Works with Article notify(&tweet); // Works with Tweet }
Both print_summary
and notify
can operate on any type that implements Summary
, demonstrating polymorphism. Under the hood, Rust typically uses static dispatch (monomorphization) for generic functions like these, meaning specialized code is generated for each concrete type (Article
and Tweet
), ensuring high performance.
11.1.3 Returning Types that Implement Traits
Just as functions can accept arguments of types implementing a trait, they can also return values specified only by the trait they implement. This is done using impl Trait
in the return type position. This technique allows a function to hide the specific concrete type it’s returning, providing encapsulation.
trait Summary { fn summarize(&self) -> String; } struct Article { title: String, content: String, } impl Summary for Article { fn summarize(&self) -> String { format!("Article: {}...", &self.title) // Simplified for brevity } } // This function returns *some* type that implements Summary. // The caller knows it implements Summary, but not the concrete type (Article). fn create_summary_item() -> impl Summary { Article { title: String::from("Return Types"), content: String::from("Using impl Trait in return position..."), } // Note: All possible return paths within the function must ultimately // return the *same* concrete type (here, always Article). } fn main() { let summary_item = create_summary_item(); println!("Created Item: {}", summary_item.summarize()); }
This approach is useful for simplifying function signatures when the concrete return type is complex or an implementation detail the caller doesn’t need to know.
11.1.4 Blanket Implementations
Rust allows implementing a trait for all types that satisfy another trait bound. This powerful feature is called a blanket implementation. It enables extending functionality across a wide range of types concisely.
A prominent example involves the standard library traits ToString
and Display
. The Display
trait is intended for formatting types in a user-facing, human-readable way; it’s the trait used by the {}
format specifier in println!
and related macros. The standard library provides a blanket implementation of ToString
for any type that implements Display
.
// From the standard library (simplified):
use std::fmt::Display;
// Implement 'ToString' for any type 'T' that already implements 'Display'.
impl<T: Display> ToString for T {
fn to_string(&self) -> String {
// This implementation leverages the existing Display implementation
// to convert the type to a String.
format!("{}", self)
}
}
Because of this blanket implementation, any type that implements Display
(like numbers, strings, and many standard library types, or your own types if you implement Display
for them) automatically gets a to_string
method for free, which provides its user-facing string representation.