Deriving and Implementing Traits: Debug and Display
About This Module
This module explores the #[derive(Debug)] attribute in detail and introduces manual
trait implementations. Understanding how derive macros work and when to implement
traits manually is important for creating user-friendly and debuggable Rust programs.
Prework
Prework Readings
Read the following sections from "The Rust Programming Language" book:
- Chapter 10.2: Traits: Defining Shared Behavior - Introduction only
- Chapter 19.5: Macros - Focus on derive macros
- Appendix C: Derivable Traits
Pre-lecture Reflections
Before class, consider these questions:
- What is the difference between
DebugandDisplayformatting? - Why might you want to manually implement a trait instead of using derive?
- How do derive macros help reduce boilerplate code?
- When would custom formatting be important for user experience?
- What role do traits play in Rust's type system?
Lecture
Learning Objectives
By the end of this module, you should be able to:
- Understand what
#[derive(Debug)]does under the hood - Manually implement the
Debugtrait for custom types - Implement the
Displaytrait for user-friendly output - Choose between
DebugandDisplayformatting appropriately - Understand when to use derive vs. manual implementation
- Apply formatting traits to make code more debuggable
Understanding #[derive(Debug)]
- A simple way to tell Rust to generate code that allows a complex type to be printed
- Here's the equivalent manual implementation of the
Debugtrait - more on traits and
impllater
use std::fmt;
impl fmt::Debug for Direction {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
Direction::North => write!(f, "North"),
Direction::East => write!(f, "East"),
Direction::South => write!(f, "South"),
Direction::West => write!(f, "West"),
}
}
}
let dir = Direction::North;
dir
North
let dir = Direction::North;
println!("{:?}",dir);
North
// Example of how make a complex datatype printable directly (without deriving from Debug)
use std::fmt;
impl fmt::Display for Direction {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
Direction::North => write!(f, "North"),
Direction::East => write!(f, "East"),
Direction::South => write!(f, "South"),
Direction::West => write!(f, "West"),
}
}
}
println!("{}", dir);
North
Best Practices
Design Guidelines:
#![allow(unused)] fn main() { #[derive(Debug, Clone, PartialEq)] pub enum UserAction { Login(String), Logout, UpdateProfile { name: String, email: String }, } impl fmt::Display for UserAction { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { UserAction::Login(username) => write!(f, "User '{}' logged in", username), UserAction::Logout => write!(f, "User logged out"), UserAction::UpdateProfile { name, .. } => { write!(f, "User '{}' updated profile", name) } } } } // Usage let action = UserAction::Login("alice".to_string()); println!("{}", action); // User-friendly: "User 'alice' logged in" println!("{:?}", action); // Debug: Login("alice") }
When to Implement Manually:
- Security: Hide sensitive information in debug output
- Performance: Optimize formatting for frequently-used types
- User Experience: Create polished output for end users
- Domain Requirements: Follow domain-specific formatting conventions