Intermediate Topics: Traits and Generics
Traits and generics are two of Rust’s most powerful features, enabling you to write flexible, reusable code without sacrificing performance or type safety. They are the foundation of Rust’s unique approach to polymorphism and abstraction.
Generics: Type Parameters for Flexible Code
Generics allow you to write code that works with multiple types while maintaining type safety. Instead of writing separate functions or structs for each type, you can use placeholders for types.
Generic Functions
// Without generics, you might write:
// fn largest_i32(list: &[i32]) -> i32 { /* ... */ }
// fn largest_char(list: &[char]) -> char { /* ... */ }
// With generics:
fn largest<T: PartialOrd + Copy>(list: &[T]) -> T {
let mut largest = list[0]; // Requires Copy trait
for &item in list.iter() { // Iterate over references and dereference with &
if item > largest { // Requires PartialOrd trait
largest = item;
}
}
largest
}
fn main() {
let number_list = vec![34, 50, 25, 100, 65];
let result = largest(&number_list);
println!("The largest number is {}", result); // Output: 100
let char_list = vec!['y', 'm', 'a', 'q'];
let result = largest(&char_list);
println!("The largest char is {}", result); // Output: y
}
<T>: DeclaresTas a generic type parameter.T: PartialOrd + Copy: These are trait bounds. They specify thatTmust implement thePartialOrdtrait (for comparison using>) and theCopytrait (because we’re copying elements).
Generic Structs and Enums
Structs and enums can also be generic, allowing them to hold values of different types.
struct Point<T> {
x: T,
y: T,
}
// Struct with multiple generic parameters
struct MultiPoint<T, U> {
x: T,
y: U,
}
fn main() {
let integer_point = Point { x: 5, y: 10 };
let float_point = Point { x: 1.0, y: 4.0 };
// let mixed_point = Point { x: 5, y: 10.4 }; // ERROR: x and y must be same type for Point<T>
let mixed_point = MultiPoint { x: 5, y: 10.4 }; // OK for MultiPoint<T, U>
println!("Integer Point: ({}, {})", integer_point.x, integer_point.y);
println!("Mixed Point: ({}, {})", mixed_point.x, mixed_point.y);
}
Rust’s standard library makes extensive use of generic enums, such as Option<T> and Result<T, E>.
Performance of Generics (Monomorphization)
One of Rust’s key advantages is that generics have zero runtime cost. The Rust compiler achieves this through a process called monomorphization.
During compilation, for every concrete type that a generic function or struct is used with, the compiler generates a specialized, non-generic version of that code. This means that at runtime, the code behaves exactly as if you had written out duplicate implementations for each type manually, with no overhead from dynamic dispatch. The trade-off can sometimes be a larger binary size due to code duplication.
Traits: Defining Shared Behavior
Traits define shared behavior that types can implement. They are similar to interfaces in other languages but with more powerful features.
Defining a Trait
pub trait Summary {
fn summarize_author(&self) -> String; // A required method
// A method with a default implementation
fn summarize(&self) -> String {
format!("(Read more from {}...)", self.summarize_author())
}
}
pub trait Summary: Declares a public trait namedSummary.- Methods listed without a body are required to be implemented by any type that implements the trait.
- Methods with a default implementation can be used as-is or overridden by the implementing type.
Implementing a Trait for a Type
pub struct NewsArticle {
pub headline: String,
pub author: String,
pub content: String,
}
impl Summary for NewsArticle {
fn summarize_author(&self) -> String {
format!("{}", self.author)
}
// We can also override the default `summarize` method
fn summarize(&self) -> String {
format!("{}: {} (by {})", self.headline, self.content, self.author)
}
}
pub struct Tweet {
pub username: String,
pub content: String,
pub reply: bool,
pub retweet: bool,
}
impl Summary for Tweet {
fn summarize_author(&self) -> String {
format!("@{}", self.username)
}
// Use the default summarize implementation for Tweet
}
fn main() {
let tweet = Tweet {
username: String::from("rustacean"),
content: String::from("Of course, as you probably already know, Rust is great!"),
reply: false,
retweet: false,
};
println!("1 new tweet: {}", tweet.summarize());
// Output: 1 new tweet: (Read more from @rustacean...)
let article = NewsArticle {
headline: String::from("The Sky is Falling!"),
author: String::from("John Doe"),
content: String::from("The sky is not actually falling, but it's an exciting headline!"),
};
println!("New article: {}", article.summarize());
// Output: New article: The Sky is Falling!: The sky is not actually falling, but it's an exciting headline! (by John Doe)
}
Traits as Parameters (Trait Bounds)
You can use traits to define trait bounds on generic type parameters, ensuring that the generic type implements certain behaviors.
impl Trait Syntax
This is a concise way to specify trait bounds for function parameters.
pub fn notify(item: &impl Summary) { // `item` must implement Summary
println!("Breaking news! {}", item.summarize());
}
// This is syntactic sugar for:
// pub fn notify<T: Summary>(item: &T) { ... }
Generic Type Parameters with Trait Bounds
For more complex scenarios, explicitly naming the generic type parameter is clearer.
pub fn notify_verbose<T: Summary>(item: &T) {
println!("Breaking news (verbose)! {}", item.summarize());
}
Multiple Trait Bounds
You can require a type to implement multiple traits using the + syntax.
use std::fmt::Display;
pub fn notify_multiple_bounds<T: Summary + Display>(item: &T) {
println!("Breaking news! {}", item.summarize());
println!("Also displayable: {}", item); // Requires Display trait
}
// `Display` is implicitly implemented for `NewsArticle` through `fmt::Debug` in some cases
// or you can implement it manually
impl Display for NewsArticle {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "Article: {}", self.headline)
}
}
// You can't use notify_multiple_bounds with Tweet unless Tweet also implements Display
// (which it currently doesn't without explicit impl Display for Tweet)
// `println!` works for `Tweet` because `Debug` is derived, not `Display`.
where Clauses
For many generic parameters or complex trait bounds, a where clause improves readability.
fn some_function<T, U>(t: &T, u: &U) -> i32
where T: Display + Clone,
U: Clone + std::fmt::Debug,
{
// ... function body
println!("t: {} (cloned: {:?}), u: {:?} (cloned: {:?})", t, t.clone(), u, u.clone());
42
}
fn main() {
let num = 10;
let text = String::from("hello");
// This will work if NewsArticle (or another type) implements Display + Clone
// And if `String` implements Clone + Debug (which it does)
// let _val = some_function(&article, &text);
}
Returning Types with impl Trait
You can also use impl Trait in the return position of a function when you want to return a type that implements a certain trait, but you don’t want to expose the concrete type. This is especially useful for returning iterators or futures.
fn returns_summarizable() -> impl Summary {
Tweet {
username: String::from("horse_ebooks"),
content: String::from("of course, as you probably already know, people"),
reply: false,
retweet: false,
}
}
// Note: This only works if the function returns *one specific concrete type*
// that implements the trait. You can't return a `Tweet` in one branch and a `NewsArticle` in another.
/*
fn returns_summarizable_error(switch: bool) -> impl Summary {
if switch {
Tweet { // ... }
} else {
NewsArticle { // ... } // ERROR: `if` and `else` have incompatible types
}
}
*/
Associated Types in Traits
Associated types allow you to specify a placeholder type within a trait definition. The concrete type for this placeholder is determined by the implementing type. This is often used when a trait logically implies a single, consistent output type.
The most common example is the Iterator trait:
pub trait Iterator {
type Item; // Associated type
fn next(&mut self) -> Option<Self::Item>;
}
Here, Item is the type of the elements produced by the iterator. A specific type (e.g., Vec<i32>) will implement Iterator only once, and Item will be fixed (e.g., i32). This removes ambiguity compared to trait Iterator<T>, which would allow multiple implementations for the same type (e.g., Vec<i32> could iterate over i32 or String if allowed, causing confusion).
Trait Objects for Dynamic Dispatch
While generics and trait bounds enable static dispatch (monomorphization at compile time), trait objects (&dyn Trait or Box<dyn Trait>) allow for dynamic dispatch (method resolution at runtime). This provides runtime polymorphism, similar to virtual functions in C++ or interfaces in Java/Go.
Trait objects store:
- A pointer to an instance of a type that implements the trait.
- A pointer to a vtable (virtual table) that contains pointers to the actual method implementations for that specific type.
// Our Summary trait defined earlier
fn print_summaries(items: &[&dyn Summary]) { // Takes a slice of trait objects
for item in items {
println!("- {}", item.summarize());
}
}
fn main() {
let tweet = Tweet {
username: String::from("rustacean"),
content: String::from("learning Rust is a journey"),
reply: false,
retweet: false,
};
let article = NewsArticle {
headline: String::from("New discoveries in systems programming"),
author: String::from("Dr. Ada Lovelace"),
content: String::from("Rust provides powerful tools for building safe and fast systems."),
};
// Create a vector of trait objects
let items_to_summarize: Vec<&dyn Summary> = vec![
&tweet, // &Tweet converts to &dyn Summary
&article, // &NewsArticle converts to &dyn Summary
];
print_summaries(&items_to_summarize);
}
&dyn Summary: This is a trait object. It means “a reference to some type that implements theSummarytrait”. The concrete type is determined at runtime.- Dynamic dispatch incurs a small runtime overhead due to the vtable lookup, but offers greater flexibility in handling heterogeneous collections.
Object Safety
Not all traits can be turned into trait objects. A trait is object safe if all the methods in the trait have the following properties:
- The method does not use
Self(the concrete type itself) as a parameter. - The method does not use
Selfas a return type (except forSelfwrapped in anOption,Result, orBox).
These rules ensure that the size of the implementing type doesn’t need to be known at compile time for the vtable to work correctly.
Blanket Implementations
Rust allows you to implement a trait for any type that satisfies certain trait bounds. These are called blanket implementations.
A common example from the standard library:
// The standard library provides this blanket implementation:
// impl<T: Display> ToString for T {
// fn to_string(&self) {
// format!("{}", self)
// }
// }
This means that any type T that implements the Display trait automatically gets an implementation of the ToString trait. This is why you can call .to_string() on an i32 or a String (which both implement Display).
Exercises / Mini-Challenges
Exercise 8.1: Generic Point Struct with Method
Create a generic Point<T> struct that holds x and y coordinates of type T.
Implement a method distance_from_origin(&self) -> f64 for Point<f64> that calculates the Euclidean distance from the origin (0.0, 0.0). This method should only be available for Point<f64>.
Instructions:
- Define the
Point<T>struct. - Implement
impl<T> Point<T> { ... }for a generic constructornew(x: T, y: T) -> Self. - Implement a specific
impl Point<f64> { ... }block for thedistance_from_originmethod.- Formula:
sqrt(x*x + y*y)
- Formula:
- In
main, create aPoint<f64>and calculate its distance. - Try to call
distance_from_originon aPoint<i32>to observe the compiler error.
// Solution Hint:
/*
struct Point<T> {
x: T,
y: T,
}
impl<T> Point<T> {
fn new(x: T, y: T) -> Self {
Point { x, y }
}
}
impl Point<f64> { // Specific implementation for f64
fn distance_from_origin(&self) -> f64 {
(self.x.powi(2) + self.y.powi(2)).sqrt()
}
}
fn main() {
let p1 = Point::new(3.0, 4.0);
println!("Distance from origin: {}", p1.distance_from_origin()); // Output: 5
let p2 = Point::new(3, 4);
// p2.distance_from_origin(); // ERROR: Method not found for `Point<i32>`
}
*/
Exercise 8.2: Printable Trait with Default Implementation
Define a trait Printable with a required method get_content(&self) -> String and a default method print_nicely(&self) that uses get_content and println!.
Implement Printable for two different structs: Book (with title: String, author: String) and BlogPost (with headline: String, url: String).
For Book, get_content should return “Title by Author”.
For BlogPost, get_content should return “Headline (URL)”.
In main, create instances of Book and BlogPost and call print_nicely() on each.
Instructions:
- Define the
Printabletrait. - Define
BookandBlogPoststructs. - Implement
Printablefor both structs, providing theget_contentmethod. - In
main, instantiate both and callprint_nicely().
// Solution Hint:
/*
trait Printable {
fn get_content(&self) -> String;
fn print_nicely(&self) {
println!("--- Content ---");
println!("{}", self.get_content());
println!("---------------");
}
}
struct Book {
title: String,
author: String,
}
impl Printable for Book {
fn get_content(&self) -> String {
format!("{} by {}", self.title, self.author)
}
}
struct BlogPost {
headline: String,
url: String,
}
impl Printable for BlogPost {
fn get_content(&self) -> String {
format!("{} ({})", self.headline, self.url)
}
// No need to implement print_nicely as default works
}
fn main() {
let book = Book {
title: String::from("The Rust Book"),
author: String::from("Steve Klabnik & Carol Nichols"),
};
book.print_nicely();
let blog_post = BlogPost {
headline: String::from("Mastering Rust Traits"),
url: String::from("https://example.com/rust-traits"),
};
blog_post.print_nicely();
}
*/
Exercise 8.3: Dynamic Shape Drawer (Trait Objects)
Define a trait Shape with methods area(&self) -> f64 and draw(&self).
Implement Shape for Circle (with radius: f64) and Rectangle (with width: f64, height: f64).
Write a function draw_all_shapes(shapes: Vec<Box<dyn Shape>>) that takes a vector of trait objects and calls both area() and draw() on each shape.
Instructions:
- Define the
Shapetrait. - Define
CircleandRectanglestructs. - Implement
Shapefor bothCircleandRectangle.Circlearea:PI * radius * radiusRectanglearea:width * heightdrawshould just print a description of the shape and its area.
- In
main, create aVec<Box<dyn Shape>>, populate it with instances ofCircleandRectangle(rememberBox::new()), and then calldraw_all_shapes.
// Solution Hint:
/*
use std::f64::consts::PI;
trait Shape {
fn area(&self) -> f64;
fn draw(&self);
}
struct Circle {
radius: f64,
}
impl Shape for Circle {
fn area(&self) -> f64 {
PI * self.radius * self.radius
}
fn draw(&self) {
println!("Drawing a Circle with radius {} and area {:.2}", self.radius, self.area());
}
}
struct Rectangle {
width: f64,
height: f64,
}
impl Shape for Rectangle {
fn area(&self) -> f64 {
self.width * self.height
}
fn draw(&self) {
println!("Drawing a Rectangle with dimensions {}x{} and area {:.2}", self.width, self.height, self.area());
}
}
fn draw_all_shapes(shapes: Vec<Box<dyn Shape>>) {
for shape in shapes {
shape.draw();
// You could also print area separately if needed: println!("Area: {:.2}", shape.area());
}
}
fn main() {
let circle = Circle { radius: 5.0 };
let rectangle = Rectangle { width: 4.0, height: 6.0 };
let mut shapes: Vec<Box<dyn Shape>> = Vec::new();
shapes.push(Box::new(circle));
shapes.push(Box::new(rectangle));
draw_all_shapes(shapes);
}
*/