Loading pageā¦
Rust walkthroughs
Loading pageā¦
strum::Display derive macro differ from implementing std::fmt::Display manually for enums?strum::Display is a derive macro that automatically implements std::fmt::Display for enums by using each variant's name as its display representation. This differs from manual Display implementation in two key ways: it eliminates boilerplate by generating the implementation automatically, and it provides customization options through attributes like #[strum(to_string = "...")] to override default behavior. Manual implementation gives you full control over output formatting and can encode arbitrary logic, while strum::Display provides a declarative approach that keeps the output format visible in the type definition itself. The choice between them depends on whether you need simple variant name mapping (use strum) or complex formatting logic (manual implementation).
use strum::Display;
#[derive(Display)]
enum Status {
Pending,
InProgress,
Completed,
Failed,
}
fn main() {
let status = Status::InProgress;
println!("{}", status); // "InProgress"
}The derive macro generates a Display implementation using variant names.
use std::fmt;
enum Status {
Pending,
InProgress,
Completed,
Failed,
}
impl fmt::Display for Status {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Status::Pending => write!(f, "Pending"),
Status::InProgress => write!(f, "InProgress"),
Status::Completed => write!(f, "Completed"),
Status::Failed => write!(f, "Failed"),
}
}
}
fn main() {
let status = Status::InProgress;
println!("{}", status); // "InProgress"
}Manual implementation requires explicit match arms for each variant.
use strum::Display;
#[derive(Display)]
enum Status {
#[strum(to_string = "Waiting for review")]
Pending,
#[strum(to_string = "Work in progress")]
InProgress,
#[strum(to_string = "Successfully completed")]
Completed,
#[strum(to_string = "Operation failed")]
Failed,
}
fn main() {
println!("{}", Status::Pending); // "Waiting for review"
println!("{}", Status::InProgress); // "Work in progress"
}#[strum(to_string = "...")] customizes the display string for each variant.
use std::fmt;
enum Status {
Pending,
InProgress,
Completed,
Failed,
}
impl fmt::Display for Status {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let s = match self {
Status::Pending => "Waiting for review",
Status::InProgress => "Work in progress",
Status::Completed => "Successfully completed",
Status::Failed => "Operation failed",
};
write!(f, "{}", s)
}
}Manual implementation achieves the same result with explicit match.
use strum::Display;
use serde::Serialize;
#[derive(Display, Serialize)]
#[strum(serialize_all = "snake_case")]
enum ErrorCode {
InvalidInput,
NetworkTimeout,
AuthenticationFailed,
InternalError,
}
fn main() {
println!("{}", ErrorCode::InvalidInput); // "invalid_input"
println!("{}", ErrorCode::AuthenticationFailed); // "authentication_failed"
}#[strum(serialize_all = "...")] applies case transformations to all variants.
use strum::Display;
#[derive(Display)]
#[strum(serialize_all = "snake_case")]
enum SnakeCase {
MyVariant, // "my_variant"
}
#[derive(Display)]
#[strum(serialize_all = "camelCase")]
enum CamelCase {
MyVariant, // "myVariant"
}
#[derive(Display)]
#[strum(serialize_all = "PascalCase")]
enum PascalCase {
MyVariant, // "MyVariant"
}
#[derive(Display)]
#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
enum ScreamingSnake {
MyVariant, // "MY_VARIANT"
}
#[derive(Display)]
#[strum(serialize_all = "kebab-case")]
enum KebabCase {
MyVariant, // "my-variant"
}
fn main() {
println!("{}", SnakeCase::MyVariant); // my_variant
println!("{}", CamelCase::MyVariant); // myVariant
println!("{}", PascalCase::MyVariant); // MyVariant
println!("{}", ScreamingSnake::MyVariant); // MY_VARIANT
println!("{}", KebabCase::MyVariant); // my-variant
}Multiple case transformations are available via serialize_all.
use std::fmt;
struct User {
name: String,
id: u64,
}
enum Permission {
Read,
Write,
Admin,
}
impl fmt::Display for Permission {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
// Can access formatter options
match self {
Permission::Read => {
if f.alternate() {
write!(f, "READ_ACCESS")
} else {
write!(f, "read")
}
}
Permission::Write => {
if f.alternate() {
write!(f, "WRITE_ACCESS")
} else {
write!(f, "write")
}
}
Permission::Admin => {
if f.alternate() {
write!(f, "ADMIN_ACCESS")
} else {
write!(f, "admin")
}
}
}
}
}
fn main() {
let perm = Permission::Read;
println!("{}", perm); // "read"
println!("{:#}", perm); // "READ_ACCESS"
}Manual implementation can use Formatter options like alternate().
use strum::Display;
#[derive(Display)]
enum Permission {
#[strum(to_string = "read")]
Read,
#[strum(to_string = "write")]
Write,
#[strum(to_string = "admin")]
Admin,
}
// strum::Display cannot access Formatter options
// Cannot implement: println!("{:#}", Permission::Read);
// All variants always produce the same stringstrum::Display generates static strings without formatter access.
use strum::Display;
#[derive(Display)]
enum Message {
#[strum(to_string = "Hello, World!")]
Hello,
#[strum(to_string = "Goodbye")]
Goodbye,
#[strum(disabled)] // Don't derive Display for this variant
Custom(String),
}
// Error: Custom variant has no Display implementation
// Must implement manually or use different approachstrum::Display has limited support for variants with data.
use std::fmt;
enum Message {
Hello,
Goodbye,
Custom(String),
}
impl fmt::Display for Message {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Message::Hello => write!(f, "Hello, World!"),
Message::Goodbye => write!(f, "Goodbye"),
Message::Custom(s) => write!(f, "{}", s),
}
}
}
fn main() {
println!("{}", Message::Hello); // "Hello, World!"
println!("{}", Message::Goodbye); // "Goodbye"
println!("{}", Message::Custom("Custom msg".into())); // "Custom msg"
}Manual implementation handles variants with data naturally.
use strum::Display;
use std::fmt;
#[derive(Display)]
enum Status {
Active,
Inactive,
Pending,
}
// Additional trait implementations alongside strum::Display
impl fmt::Debug for Status {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Status::Active => write!(f, "Status::Active"),
Status::Inactive => write!(f, "Status::Inactive"),
Status::Pending => write!(f, "Status::Pending"),
}
}
}
fn main() {
let status = Status::Active;
println!("Display: {}", status); // "Active"
println!("Debug: {:?}", status); // "Status::Active"
}strum::Display coexists with manual implementations of other traits.
use strum::Display;
#[derive(Display)]
enum HttpError {
#[strum(to_string = "Bad Request")]
BadRequest,
#[strum(to_string = "Unauthorized")]
Unauthorized,
#[strum(to_string = "Not Found")]
NotFound,
#[strum(to_string = "Internal Server Error")]
InternalError,
}
fn main() {
let err = HttpError::NotFound;
println!("Error: {}", err); // "Error: Not Found"
}Each variant can have its own display string.
use strum::Display;
// strum keeps display strings visible in the enum definition
#[derive(Display)]
enum LogLevel {
Debug, // Display: "Debug"
Info, // Display: "Info"
Warning, // Display: "Warning"
Error, // Display: "Error"
}
// With manual implementation, you must look at impl block
// to see what strings are producedstrum::Display keeps the mapping visible alongside variant definitions.
use strum::{Display, EnumString};
#[derive(Display, EnumString)]
#[strum(serialize_all = "snake_case")]
enum Color {
Red,
Green,
Blue,
}
fn main() {
// Display produces snake_case
let color = Color::Red;
println!("{}", color); // "red"
// EnumString parses snake_case
let parsed: Color = "red".parse().unwrap();
assert_eq!(parsed, Color::Red);
}Combine Display with EnumString for round-trip parsing.
use strum::{Display, EnumString};
#[derive(Display, EnumString, Debug, PartialEq)]
#[strum(serialize_all = "snake_case")]
enum State {
NotStarted,
InProgress,
Completed,
}
fn main() {
// Display -> String
let state = State::InProgress;
let display = format!("{}", state); // "in_progress"
// String -> FromStr
let parsed: State = display.parse().unwrap();
assert_eq!(state, parsed);
}Consistent serialize_all ensures Display and FromStr align.
use std::fmt;
enum Money {
Dollars(u32),
Euros(u32),
Yen(u32),
}
impl fmt::Display for Money {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Money::Dollars(amount) => {
if *amount == 1 {
write!(f, "${}", amount)
} else {
write!(f, "${} dollars", amount)
}
}
Money::Euros(amount) => write!(f, "ā¬{}", amount),
Money::Yen(amount) => write!(f, "Ā„{}", amount),
}
}
}
fn main() {
println!("{}", Money::Dollars(1)); // "$1"
println!("{}", Money::Dollars(5)); // "$5 dollars"
println!("{}", Money::Euros(10)); // "ā¬10"
}Manual implementation handles conditional formatting logic.
// What strum::Display generates:
#[derive(Display)]
enum Status {
Active,
Inactive,
}
// Approximately generates:
impl std::fmt::Display for Status {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Status::Active => write!(f, "Active"),
Status::Inactive => write!(f, "Inactive"),
}
}
}The macro generates standard Display implementations.
use strum::Display;
#[derive(Display)]
enum Fast {
A,
B,
C,
}
// strum::Display generates:
// - Static strings (no allocation)
// - Simple match expression
// - Same performance as manual implementation
// Manual implementation with formatting:
impl std::fmt::Display for Fast {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
// Can potentially allocate or compute
let s = format!("{:?}", self); // Allocation
write!(f, "{}", s)
}
}strum::Display uses static strings; manual can introduce allocations.
use strum::Display;
#[derive(Display)]
enum Notification {
#[strum(to_string = "You have a new message")]
NewMessage,
#[strum(to_string = "You have {0} unread messages")]
UnreadCount(u32), // This won't work - strum doesn't support this
}
// strum::Display doesn't support string interpolation
// For variants with data, use manual implementationstrum::Display cannot use variant data in output strings.
use std::fmt;
enum Notification {
NewMessage,
UnreadCount(u32),
CustomMessage(String),
}
impl fmt::Display for Notification {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Notification::NewMessage => write!(f, "You have a new message"),
Notification::UnreadCount(n) => write!(f, "You have {} unread messages", n),
Notification::CustomMessage(msg) => write!(f, "{}", msg),
}
}
}
fn main() {
println!("{}", Notification::UnreadCount(5)); // "You have 5 unread messages"
}Manual implementation accesses variant data for formatted output.
use strum::Display;
// Good use case: simple variant-to-string mapping
#[derive(Display)]
enum HttpStatus {
#[strum(to_string = "200 OK")]
Ok,
#[strum(to_string = "201 Created")]
Created,
#[strum(to_string = "400 Bad Request")]
BadRequest,
#[strum(to_string = "404 Not Found")]
NotFound,
#[strum(to_string = "500 Internal Server Error")]
InternalError,
}Use strum::Display for simple, static string mappings.
use std::fmt;
// Use manual when:
// 1. Variant data affects output
// 2. Formatter options change output
// 3. Complex logic required
// 4. Dynamic string construction
enum Error {
Io(std::io::Error),
Parse(String),
Network { code: u16, message: String },
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Error::Io(e) => write!(f, "IO error: {}", e),
Error::Parse(s) => write!(f, "Parse error: {}", s),
Error::Network { code, message } => write!(f, "Network error ({}): {}", code, message),
}
}
}Use manual Display for complex formatting or data-dependent output.
| Feature | strum::Display | Manual Display |
|---------|------------------|-------------------|
| Boilerplate | Minimal | More code |
| Variant data access | No | Yes |
| Formatter options | No | Yes |
| Conditional logic | No | Yes |
| Case transformations | Built-in | Manual |
| Visibility | In enum | In impl block |
| Maintenance | Easier | More error-prone |
strum::Display is ideal for simple variant-to-string mappings:
Display implementations automaticallyserialize_all for case transformationsEnumString for round-trip parsingManual Display is necessary when you need:
{:#} to change behaviorDecision guide: If your Display implementation is a simple match that maps variants to static strings, strum::Display reduces boilerplate and keeps the mapping visible. If you need to format variant data, use formatter options, or implement complex logic, write the Display implementation manually. The macro generates the same code you would write by hand for the simple casesāit's purely a convenience for reducing repetitive match expressions.