In Rust, traits define shared behavior. If a type implements a trait, it can use that trait's methods. Some traits are simple enough that writing the implementation manually is busywork. Derive macros handle this.
#[derive(Debug)]
struct Point {
x: i32,
y: i32,
}
fn main() {
let p = Point { x: 10, y: 20 };
println!("{:?}", p); // Point { x: 10, y: 20 }
}
The #[derive(Debug)] attribute tells the compiler to automatically implement the Debug trait for Point. Without it, you'd write the implementation yourself or not be able to print the struct for debugging.
What Is the Debug Trait?
The Debug trait provides formatted output for debugging. It's part of the standard library:
pub trait Debug {
fn fmt(&self, f: &mut Formatter<'_>) -> Result;
}
When you use {:?} in a format string, Rust calls the fmt method. The derive macro generates a sensible default implementation that prints the struct's name and fields.
Without Debug, trying to print a struct gives a compiler error:
struct Point {
x: i32,
y: i32,
}
let p = Point { x: 10, y: 20 };
println!("{:?}", p); // Error: Point doesn't implement Debug
Rust doesn't assume you want everything printable. You opt in by deriving Debug.
Pretty Printing with {:#?}
The {:#?} format specifier provides pretty-printed output:
#[derive(Debug)]
struct User {
name: String,
age: u32,
address: Address,
}
#[derive(Debug)]
struct Address {
street: String,
city: String,
}
let user = User {
name: "Alice".to_string(),
age: 30,
address: Address {
street: "123 Main St".to_string(),
city: "Portland".to_string(),
},
};
println!("{:#?}", user);
Output:
User {
name: "Alice",
age: 30,
address: Address {
street: "123 Main St",
city: "Portland",
},
}
This is readable for nested structures. Use {:#?} when inspecting complex data.
Other Derivable Traits
Several standard library traits can be derived:
#[derive(Debug, Clone, PartialEq, Eq)]
struct Point {
x: i32,
y: i32,
}
Common derivable traits:
- Debug: Format output for debugging
- Clone: Create copies of values
- Copy: Enable bitwise copying (requires Clone, works for simple types)
-
PartialEq: Enable
==and!=comparisons - Eq: Marker trait for full equivalence (requires PartialEq)
-
PartialOrd: Enable
<,>,<=,>=comparisons - Ord: Total ordering (requires PartialOrd and Eq)
- Hash: Compute hash values for use in HashMap/HashSet
- Default: Provide a default value for the type
Each has specific requirements. For example, Copy only works for types where all fields are Copy. You can't derive Copy for a struct containing a String, because String isn't Copy.
How Derive Macros Work
Derive macros are procedural macros. They run at compile time, inspect the struct or enum definition, and generate code.
For #[derive(Debug)], the macro generates something like:
impl Debug for Point {
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
f.debug_struct("Point")
.field("x", &self.x)
.field("y", &self.y)
.finish()
}
}
You don't see this code, but the compiler inserts it. The result is the same as if you'd written it manually.
Deriving for Enums
Derive works for enums too:
#[derive(Debug)]
enum Status {
Active,
Inactive,
Pending(String),
}
let status = Status::Pending("review".to_string());
println!("{:?}", status); // Pending("review")
The derived implementation handles all variants correctly.
Limitations of Derive
Derive macros work when the implementation is mechanical—when the trait can be implemented by iterating over fields. They don't work when custom logic is needed.
For example, you might want a custom Debug implementation that hides sensitive fields:
struct User {
name: String,
password: String,
}
impl Debug for User {
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
f.debug_struct("User")
.field("name", &self.name)
.field("password", &"***")
.finish()
}
}
This prints password: "***" instead of the actual password. Derive can't do this—you need to implement the trait manually.
Custom Derive Macros
You can write your own derive macros. This requires procedural macro crates and is more involved than using existing derives.
Libraries like serde provide custom derives:
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize, Debug)]
struct Config {
host: String,
port: u16,
}
The Serialize and Deserialize derives generate JSON serialization code automatically.
When to Derive vs Implement
Derive when:
- The default behavior is what you want
- All fields support the trait
- You're prototyping and need something quick
Implement manually when:
- Custom logic is required
- Some fields should be excluded or handled specially
- Performance matters and the derived version isn't optimal
Start with derive. Switch to manual implementation when you need control.
Debug vs Display
Rust has two formatting traits: Debug and Display.
Debug ({:?}) is for developers. It shows the internal structure of types. You derive it for debugging.
Display ({}) is for users. It shows a human-readable representation. You implement it manually:
use std::fmt;
struct Point {
x: i32,
y: i32,
}
impl fmt::Display for Point {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "({}, {})", self.x, self.y)
}
}
let p = Point { x: 10, y: 20 };
println!("{}", p); // (10, 20)
Display can't be derived—it requires deciding how to present the type to users, which is a design choice, not a mechanical process.
Common Derive Combinations
Typical patterns:
// Simple data structures
#[derive(Debug, Clone, PartialEq, Eq)]
// Configuration structs
#[derive(Debug, Clone, Serialize, Deserialize)]
// Types used in collections
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
// Types with default values
#[derive(Debug, Default)]
These combinations cover most use cases.
Further Reading
The Rust reference's section on derive lists all derivable traits and their requirements.
For writing custom derive macros, see the Rust Book chapter on macros.
The Debug trait documentation explains formatting options and customization.
Derive macros save time and reduce boilerplate.
0 comments