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

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

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.


← PreviousNext →