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
.