Rust - Traits (Basics)
Overview
Estimated time: 50ā65 minutes
Learn Rust's trait system for defining shared behavior across types. Master trait implementation, trait bounds, and default implementations to write flexible and reusable code.
Learning Objectives
- Define traits to specify shared behavior
- Implement traits for custom and built-in types
- Use trait bounds to constrain generic parameters
- Create default implementations in traits
- Understand common standard library traits
Prerequisites
What are Traits?
Traits define shared behavior that multiple types can implement. They're similar to interfaces in other languages:
// Define a trait
trait Summary {
fn summarize(&self) -> String;
}
// Implement the trait for a struct
struct Article {
headline: String,
location: String,
author: String,
content: String,
}
impl Summary for Article {
fn summarize(&self) -> String {
format!("{}, by {} ({})", self.headline, self.author, self.location)
}
}
struct Tweet {
username: String,
content: String,
reply: bool,
retweet: bool,
}
impl Summary for Tweet {
fn summarize(&self) -> String {
format!("{}: {}", self.username, self.content)
}
}
fn main() {
let article = Article {
headline: "Rust Programming".to_string(),
location: "San Francisco".to_string(),
author: "Jane Doe".to_string(),
content: "Rust is a systems programming language...".to_string(),
};
let tweet = Tweet {
username: "john_doe".to_string(),
content: "Learning Rust is amazing!".to_string(),
reply: false,
retweet: false,
};
println!("Article summary: {}", article.summarize());
println!("Tweet summary: {}", tweet.summarize());
}
Expected output:
Article summary: Rust Programming, by Jane Doe (San Francisco)
Tweet summary: john_doe: Learning Rust is amazing!
Default Implementations
Traits can provide default implementations that types can use or override:
trait Summary {
fn summarize_author(&self) -> String;
fn summarize(&self) -> String {
format!("(Read more from {}...)", self.summarize_author())
}
}
struct BlogPost {
title: String,
author: String,
content: String,
}
impl Summary for BlogPost {
fn summarize_author(&self) -> String {
format!("@{}", self.author)
}
// Using default implementation for summarize()
}
struct Book {
title: String,
author: String,
pages: u32,
}
impl Summary for Book {
fn summarize_author(&self) -> String {
self.author.clone()
}
// Override the default implementation
fn summarize(&self) -> String {
format!("{} by {} ({} pages)", self.title, self.author, self.pages)
}
}
fn main() {
let blog = BlogPost {
title: "Rust Ownership".to_string(),
author: "alice".to_string(),
content: "Ownership is Rust's most unique feature...".to_string(),
};
let book = Book {
title: "The Rust Programming Language".to_string(),
author: "Steve Klabnik".to_string(),
pages: 552,
};
println!("Blog: {}", blog.summarize());
println!("Book: {}", book.summarize());
}
Expected output:
Blog: (Read more from @alice...)
Book: The Rust Programming Language by Steve Klabnik (552 pages)
Traits as Parameters
You can use traits to accept any type that implements a specific trait:
// Function that accepts any type implementing Summary
fn notify(item: &impl Summary) {
println!("Breaking news! {}", item.summarize());
}
// Alternative syntax using trait bounds
fn notify_verbose<T: Summary>(item: &T) {
println!("Breaking news! {}", item.summarize());
}
// Multiple trait bounds
trait Display {
fn display(&self) -> String;
}
impl Display for Article {
fn display(&self) -> String {
format!("š° {}\nš {}\nāļø {}\n\n{}",
self.headline, self.location, self.author, self.content)
}
}
fn notify_and_display<T: Summary + Display>(item: &T) {
println!("Summary: {}", item.summarize());
println!("Full display:\n{}", item.display());
}
fn main() {
let article = Article {
headline: "Rust 2024 Edition Released".to_string(),
location: "Global".to_string(),
author: "Rust Team".to_string(),
content: "The new edition includes many improvements...".to_string(),
};
notify(&article);
notify_and_display(&article);
}
Expected output:
Breaking news! Rust 2024 Edition Released, by Rust Team (Global)
Summary: Rust 2024 Edition Released, by Rust Team (Global)
Full display:
š° Rust 2024 Edition Released
š Global
āļø Rust Team
The new edition includes many improvements...
Returning Trait Objects
// Return types that implement a trait
fn returns_summarizable(switch: bool) -> Box<dyn Summary> {
if switch {
Box::new(Article {
headline: "Dynamic Article".to_string(),
location: "Everywhere".to_string(),
author: "AI Writer".to_string(),
content: "This article was generated dynamically...".to_string(),
})
} else {
Box::new(Tweet {
username: "bot_account".to_string(),
content: "Dynamic tweet content".to_string(),
reply: false,
retweet: false,
})
}
}
fn main() {
let item1 = returns_summarizable(true);
let item2 = returns_summarizable(false);
println!("Item 1: {}", item1.summarize());
println!("Item 2: {}", item2.summarize());
}
Expected output:
Item 1: Dynamic Article, by AI Writer (Everywhere)
Item 2: bot_account: Dynamic tweet content
Common Standard Library Traits
Clone and Copy
// Clone allows explicit duplication
#[derive(Clone)]
struct Point {
x: i32,
y: i32,
}
// Copy allows implicit duplication (for simple types)
#[derive(Copy, Clone)] // Copy requires Clone
struct Coordinate {
x: f64,
y: f64,
}
fn main() {
let p1 = Point { x: 1, y: 2 };
let p2 = p1.clone(); // Explicit clone
println!("p1: ({}, {}), p2: ({}, {})", p1.x, p1.y, p2.x, p2.y);
let c1 = Coordinate { x: 1.0, y: 2.0 };
let c2 = c1; // Implicit copy
let c3 = c1; // c1 is still usable because Coordinate implements Copy
println!("c1: ({}, {}), c2: ({}, {})", c1.x, c1.y, c2.x, c2.y);
}
Debug and Display
use std::fmt;
#[derive(Debug)]
struct Person {
name: String,
age: u32,
}
impl fmt::Display for Person {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{} (age {})", self.name, self.age)
}
}
fn main() {
let person = Person {
name: "Alice".to_string(),
age: 30,
};
println!("Debug: {:?}", person); // Uses Debug trait
println!("Display: {}", person); // Uses Display trait
println!("Pretty Debug: {:#?}", person); // Pretty print
}
Expected output:
Debug: Person { name: "Alice", age: 30 }
Display: Alice (age 30)
Pretty Debug: Person {
name: "Alice",
age: 30,
}
PartialEq and Ord
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
struct Student {
name: String,
grade: u32,
}
fn main() {
let alice = Student { name: "Alice".to_string(), grade: 85 };
let bob = Student { name: "Bob".to_string(), grade: 92 };
let charlie = Student { name: "Charlie".to_string(), grade: 85 };
println!("alice == charlie: {}", alice == charlie);
println!("alice < bob: {}", alice < bob);
println!("bob > alice: {}", bob > alice);
let mut students = vec![alice, bob, charlie];
students.sort();
println!("Sorted students: {:#?}", students);
}
Custom Trait Implementation
// Define a trait for calculating area
trait Area {
fn area(&self) -> f64;
fn perimeter(&self) -> f64;
// Default implementation using other methods
fn describe(&self) -> String {
format!("Area: {:.2}, Perimeter: {:.2}", self.area(), self.perimeter())
}
}
struct Rectangle {
width: f64,
height: f64,
}
struct Circle {
radius: f64,
}
impl Area for Rectangle {
fn area(&self) -> f64 {
self.width * self.height
}
fn perimeter(&self) -> f64 {
2.0 * (self.width + self.height)
}
}
impl Area for Circle {
fn area(&self) -> f64 {
std::f64::consts::PI * self.radius * self.radius
}
fn perimeter(&self) -> f64 {
2.0 * std::f64::consts::PI * self.radius
}
// Override default implementation
fn describe(&self) -> String {
format!("Circle - Radius: {:.2}, Area: {:.2}, Circumference: {:.2}",
self.radius, self.area(), self.perimeter())
}
}
fn print_shape_info<T: Area>(shape: &T) {
println!("{}", shape.describe());
}
fn main() {
let rect = Rectangle { width: 5.0, height: 3.0 };
let circle = Circle { radius: 2.5 };
print_shape_info(&rect);
print_shape_info(&circle);
// Using shapes in a vector
let shapes: Vec<Box<dyn Area>> = vec![
Box::new(Rectangle { width: 4.0, height: 6.0 }),
Box::new(Circle { radius: 3.0 }),
];
for (i, shape) in shapes.iter().enumerate() {
println!("Shape {}: {}", i + 1, shape.describe());
}
}
Expected output:
Area: 15.00, Perimeter: 16.00
Circle - Radius: 2.50, Area: 19.63, Circumference: 15.71
Shape 1: Area: 24.00, Perimeter: 20.00
Shape 2: Circle - Radius: 3.00, Area: 28.27, Circumference: 18.85
Where Clauses
For complex trait bounds, use where
clauses for better readability:
use std::fmt::Display;
// This is hard to read
fn complex_function<T: Display + Clone, U: Clone + Debug>(t: &T, u: &U) -> String {
format!("{} - {:?}", t, u)
}
// This is clearer with where clause
fn complex_function_clear<T, U>(t: &T, u: &U) -> String
where
T: Display + Clone,
U: Clone + Debug,
{
format!("{} - {:?}", t, u)
}
fn main() {
let text = "Hello".to_string();
let number = 42;
let result = complex_function_clear(&text, &number);
println!("{}", result);
}
Expected output:
Hello - 42
Common Pitfalls
1. Orphan Rule Violations
// This won't compile - you can't implement external traits for external types
// impl Display for Vec<i32> { ... } // Error!
// You can implement:
// - Your trait for any type
// - Any trait for your type
// - External trait for external type using the newtype pattern
struct MyVec(Vec<i32>);
impl Display for MyVec {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:?}", self.0)
}
}
2. Forgetting Trait Bounds
// This won't compile
fn print_item<T>(item: T) {
println!("{}", item); // Error: T doesn't implement Display
}
// Correct version
fn print_item<T: Display>(item: T) {
println!("{}", item);
}
Best Practices
- Use descriptive trait names -
Drawable
,Serializable
,Comparable
- Keep traits focused - Single responsibility principle applies
- Provide default implementations - When it makes sense
- Use trait bounds judiciously - Don't over-constrain your generics
- Consider trait objects - For heterogeneous collections
Checks for Understanding
Question 1: Trait vs Struct
What's the difference between a trait and a struct?
Answer
A struct defines data (fields) and can have methods. A trait defines behavior (method signatures) that multiple types can implement. Traits enable polymorphism and code reuse across different types.
Question 2: Default Implementations
When would you provide a default implementation in a trait?
Answer
Provide default implementations when there's a common way to implement a method that most types would use, or when you can implement the method in terms of other trait methods. This reduces boilerplate for implementors.
Question 3: Trait Objects
When would you use Box<dyn Trait>
instead of generic parameters?
Answer
Use trait objects when you need to store different types that implement the same trait in the same collection, or when the concrete type isn't known at compile time. Use generics when you know the type and want zero-cost abstractions.