11.1 Traits in Rust
A trait is Rust’s way of defining a collection of methods that a type must implement. This concept closely resembles interfaces in Java or abstract base classes in C++, though it is a bit more flexible. In C, one might rely on function pointers embedded in structs to achieve a similar effect, but Rust’s trait system provides more compile-time checks and safety guarantees.
Key Concepts
- Definition: A trait outlines one or more methods that a type must implement.
- Purpose: Traits enable both code reuse and abstraction by letting functions and data structures operate on any type that implements the required trait.
- Polymorphism: Traits allow treating different types uniformly, as long as those types implement the same trait. This approach provides polymorphism akin to inheritance in languages like C++—but without a large class hierarchy.
11.1.1 Declaring Traits
Declare a trait using the trait keyword, followed by the trait name and a block containing the method signatures. Traits can include default method implementations, but a type is free to override those defaults:
trait TraitName {
fn method_name(&self);
// Additional method signatures...
}
Example:
trait Summary {
fn summarize(&self) -> String;
}
Any type that implements Summary must provide a summarize method returning a String.
11.1.2 Implementing Traits
Implement a trait for a specific type using impl <Trait> for <Type>:
impl TraitName for TypeName {
fn method_name(&self) {
// Method implementation
}
}
Example
#![allow(unused)] fn main() { struct Article { title: String, content: String, } impl Summary for Article { fn summarize(&self) -> String { format!("{}...", &self.content[..50]) } } }
The Article struct implements the Summary trait by defining a summarize method.
Implementing Multiple Traits
A single type can implement multiple traits. Each trait is implemented in its own impl block, allowing you to piece together a variety of behaviors in a modular fashion.
11.1.3 Default Implementations
Traits can supply default method bodies. If an implementing type does not provide its own method, the trait’s default behavior will be used:
#![allow(unused)] fn main() { trait Greet { fn say_hello(&self) { println!("Hello!"); } } struct Person { name: String, } impl Greet for Person {} }
In this case, Person relies on the default say_hello. To override it:
impl Greet for Person {
fn say_hello(&self) {
println!("Hello, {}!", self.name);
}
}
11.1.4 Trait Bounds
Trait bounds specify that a generic type must implement a certain trait. This ensures the type has the methods or behavior the function needs. For example:
fn print_summary<T: Summary>(item: &T) {
println!("{}", item.summarize());
}
T: Summary tells the compiler that T implements Summary, guaranteeing the presence of a summarize method.
11.1.5 Traits as Parameters
A more concise way to express a trait bound in function parameters uses impl <Trait>:
fn notify(item: &impl Summary) {
println!("Breaking news! {}", item.summarize());
}
This is shorthand for fn notify<T: Summary>(item: &T).
11.1.6 Returning Types that Implement Traits
Functions can declare they return a type implementing a trait by using -> impl Trait:
fn create_summary() -> impl Summary {
Article {
title: String::from("Generics in Rust"),
content: String::from("Generics allow for code reuse..."),
}
}
All return paths in such a function must yield the same concrete type, though they share the trait implementation.
11.1.7 Blanket Implementations
A blanket implementation provides a trait implementation for all types satisfying certain bounds, letting you expand functionality across many types:
use std::fmt::Display;
impl<T: Display> ToString for T {
fn to_string(&self) -> String {
format!("{}", self)
}
}
Here, any type T implementing Display automatically gets an implementation of ToString.