What are the trade-offs between strum::Display and IntoStaticStr for string representation of enum variants?
Display provides runtime string representation via the Display trait returning an owned String, while IntoStaticStr provides compile-time static string references via conversion to &'static str. The trade-off centers on allocation versus flexibility: Display allows runtime customization and works with format! macros, while IntoStaticStr is zero-allocation but returns only the variant name as a static string. The choice depends on whether you need dynamic formatting or maximum performance with static lifetimes.
Basic Usage of Display
use strum::Display;
#[derive(Display)]
enum Status {
Active,
Inactive,
Pending,
}
fn display_example() {
let status = Status::Active;
// Display derives std::fmt::Display
// The to_string() method returns an owned String
let s: String = status.to_string();
assert_eq!(s, "Active");
// Works with format! and println!
println!("Status: {}", status);
// Works with format specifiers
let lower = format!("{}", status);
assert_eq!(lower, "Active");
// Returns owned String - has allocation cost
let owned: String = status.to_string();
}Display derives the standard std::fmt::Display trait, enabling use with any formatting infrastructure.
Basic Usage of IntoStaticStr
use strum::IntoStaticStr;
#[derive(IntoStaticStr)]
enum Status {
Active,
Inactive,
Pending,
}
fn into_static_str_example() {
let status = Status::Active;
// IntoStaticStr implements Into<&'static str>
// Returns a reference to static string - zero allocation
let s: &'static str = status.into();
assert_eq!(s, "Active");
// Can also use with reference
let s: &'static str = (&status).into();
assert_eq!(s, "Active");
// The string is determined at compile time
// No runtime string construction
// Lifetime is 'static - lives for entire program
}IntoStaticStr converts to &'static str with no runtime allocation.
Type Signatures and Traits
use strum::{Display, IntoStaticStr};
use std::fmt::Display as StdDisplay;
#[derive(Display, IntoStaticStr)]
enum Color {
Red,
Green,
Blue,
}
fn type_signatures() {
let color = Color::Red;
// Display: implements std::fmt::Display
// fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result
// This enables:
// 1. to_string() -> String (from ToString trait)
// 2. format!("{}", x) works
// 3. println!("{}", x) works
// 4. write! macro works
let string: String = color.to_string(); // Owned String
let formatted = format!("Color: {}", color); // Owned String
// IntoStaticStr: implements Into<&'static str>
// fn into(self) -> &'static str
// This enables:
// 1. .into() to get &'static str
// 2. Zero allocation
// 3. Static lifetime guarantees
let static_str: &'static str = color.into(); // No allocation
// Key difference in return type:
// Display::to_string() -> String (owned, heap allocated)
// IntoStaticStr::into() -> &'static str (borrowed, static)
}The core distinction: Display returns owned String, IntoStaticStr returns borrowed &'static str.
Performance Characteristics
use strum::{Display, IntoStaticStr};
#[derive(Display, IntoStaticStr, Clone, Copy)]
enum Status {
Active,
Inactive,
}
fn performance_comparison() {
let status = Status::Active;
// Display: allocation for each conversion
// to_string() allocates a new String
for _ in 0..1000 {
let _s = status.to_string(); // 1000 allocations
}
// IntoStaticStr: zero allocation
// into() returns a reference to static data
for _ in 0..1000 {
let _s: &'static str = status.into(); // 0 allocations
}
// Benchmark difference:
// Display: ~50ns per call (allocation + string construction)
// IntoStaticStr: ~1ns per call (just returning pointer)
}
fn memory_usage() {
// Display produces owned strings
let strings: Vec<String> = (0..100)
.map(|_| Status::Active.to_string())
.collect();
// Each String is a heap allocation
// IntoStaticStr produces static references
let refs: Vec<&'static str> = (0..100)
.map(|_| Status::Active.into())
.collect();
// No heap allocations, just copying pointers
}IntoStaticStr is significantly faster due to zero allocation and static string lookup.
Customizing Display Output
use strum::Display;
#[derive(Display)]
enum Status {
// Customize the display string with strum attribute
#[strum(to_string = "Status: Active")]
Active,
#[strum(to_string = "Status: Inactive")]
Inactive,
#[strum(to_string = "Status: Pending Review")]
Pending,
}
fn custom_display() {
let status = Status::Active;
assert_eq!(status.to_string(), "Status: Active");
// Display supports arbitrary string customization
let pending = Status::Pending;
assert_eq!(pending.to_string(), "Status: Pending Review");
}
#[derive(Display)]
#[strum(serialize_all = "snake_case")]
enum ApiEndpoint {
GetUser,
CreateUser,
DeleteUserById,
}
fn serialize_all_example() {
// strum supports various case transformations
assert_eq!(ApiEndpoint::GetUser.to_string(), "get_user");
assert_eq!(ApiEndpoint::CreateUser.to_string(), "create_user");
assert_eq!(ApiEndpoint::DeleteUserById.to_string(), "delete_user_by_id");
// Other serialize_all options:
// - "lowercase"
// - "UPPERCASE"
// - "title_case"
// - "camelCase"
// - "PascalCase"
// - "kebab-case"
// - "SCREAMING_SNAKE_CASE"
// etc.
}Display supports runtime string customization via attributes, offering flexibility.
Customizing IntoStaticStr Output
use strum::IntoStaticStr;
#[derive(IntoStaticStr)]
enum Status {
// into_static_str also supports strum attributes
#[strum(to_string = "STATUS_ACTIVE")]
Active,
#[strum(to_string = "STATUS_INACTIVE")]
Inactive,
}
fn custom_into_static_str() {
let status = Status::Active;
let s: &'static str = status.into();
assert_eq!(s, "STATUS_ACTIVE");
}
#[derive(IntoStaticStr)]
#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
enum EventType {
UserLogin,
UserLogout,
DataSync,
}
fn serialize_all_static() {
// serialize_all works with IntoStaticStr too
let event = EventType::UserLogin;
let s: &'static str = event.into();
assert_eq!(s, "USER_LOGIN");
// Note: The strings must be valid at compile time
// Custom attributes must result in static string constants
}IntoStaticStr also supports customization attributes while maintaining static lifetime guarantees.
Use Case Comparison
use strum::{Display, IntoStaticStr};
#[derive(Display, IntoStaticStr)]
enum HttpMethod {
Get,
Post,
Put,
Delete,
}
fn use_cases() {
let method = HttpMethod::Get;
// Use Display when:
// 1. You need to work with format! macros
let log = format!("Request method: {}", method);
// 2. You need String for storage or manipulation
let methods: Vec<String> = vec![
HttpMethod::Get.to_string(),
HttpMethod::Post.to_string(),
];
let concatenated = methods.join(", ");
// 3. You need to pass to APIs expecting String
fn takes_string(s: String) { /* ... */ }
takes_string(method.to_string());
// 4. You need custom formatting in attributes
// (with #[strum(to_string = "...")] attributes)
}
fn use_cases_static_str() {
let method = HttpMethod::Get;
// Use IntoStaticStr when:
// 1. You need maximum performance (zero allocation)
fn log_method(method: &'static str) {
println!("Method: {}", method);
}
log_method(method.into());
// 2. You need &'static str for const contexts or APIs
fn takes_static_str(s: &'static str) { /* ... */ }
takes_static_str(method.into());
// 3. You're comparing against static strings
if method.into() == "Get" {
println!("GET request");
}
// 4. You need to use as map keys without allocation
use std::collections::HashMap;
let mut map: HashMap<&'static str, i32> = HashMap::new();
map.insert(method.into(), 42);
}Choose based on whether you need String flexibility or &'static str performance.
Integration with Other Traits
use strum::{Display, IntoStaticStr, EnumString, AsRefStr};
#[derive(Display, IntoStaticStr, EnumString, AsRefStr)]
enum Status {
Active,
Inactive,
}
fn trait_integration() {
// strum provides multiple string-related traits:
// Display: Owned String via Display trait
let display: String = Status::Active.to_string();
// IntoStaticStr: &'static str via Into trait
let static_str: &'static str = Status::Active.into();
// AsRefStr: &str via AsRef trait (borrowed, not static)
let as_ref: &str = Status::Active.as_ref();
// EnumString: Parse from string via FromStr
let parsed: Status = "Active".parse().unwrap();
// Common combinations:
// Display + EnumString: Bidirectional string conversion
// IntoStaticStr + EnumString: Zero-allocation bidirectional
// Comparison:
// βββββββββββββββββ¬βββββββββββββββββ¬ββββββββββββββββββββββββββ
// β Trait β Returns β Use case β
// βββββββββββββββββΌβββββββββββββββββΌββββββββββββββββββββββββββ€
// β Display β String β Format, storage β
// β IntoStaticStr β &'static str β Performance, static β
// β AsRefStr β &str β Borrowed view β
// β EnumString β Self (parse) β String to enum β
// βββββββββββββββββ΄βββββββββββββββββ΄ββββββββββββββββββββββββββ
}strum provides a family of related traits for different string conversion needs.
Lifetime Differences
use strum::{Display, IntoStaticStr};
#[derive(Display, IntoStaticStr)]
enum Message {
Hello,
Goodbye,
}
fn lifetime_demonstration() {
// Display: String owns its data
let string: String = Message::Hello.to_string();
// String has owned data, lives as long as variable
// IntoStaticStr: &'static str is borrowed from static storage
let static_str: &'static str = Message::Hello.into();
// Lives for entire program duration
// Implications:
// 1. &'static str can be stored in statics
static KNOWN_MESSAGE: &str = "Hello";
// 2. &'static str can outlive the enum
fn get_static_message(msg: Message) -> &'static str {
msg.into() // Returned reference outlives msg
}
// 3. String must be cloned for ownership transfer
fn get_owned_message(msg: Message) -> String {
msg.to_string() // Already owned
}
// 4. &'static str is implicitly Copy
let s1: &'static str = Message::Hello.into();
let s2: &'static str = Message::Hello.into();
// Both point to same static string
}The 'static lifetime of IntoStaticStr enables patterns impossible with borrowed references.
Working with Collections
use strum::{Display, IntoStaticStr};
#[derive(Display, IntoStaticStr, Clone)]
enum Priority {
High,
Medium,
Low,
}
fn collections_display() {
// Display: Creates owned Strings
let priorities = vec![Priority::High, Priority::Medium, Priority::Low];
// Converting to Vec<String> allocates for each
let strings: Vec<String> = priorities.iter()
.map(|p| p.to_string())
.collect();
// Joining requires String
let joined = strings.join(", ");
assert_eq!(joined, "High, Medium, Low");
}
fn collections_static_str() {
// IntoStaticStr: No allocation
let priorities = vec![Priority::High, Priority::Medium, Priority::Low];
// Converting to Vec<&'static str> - just pointers
let refs: Vec<&'static str> = priorities.iter()
.map(|p| p.into())
.collect();
// Can still join (as_ref works)
let joined = refs.join(", ");
assert_eq!(joined, "High, Medium, Low");
// Using as map keys (efficient)
use std::collections::HashMap;
let mut counts: HashMap<&'static str, usize> = HashMap::new();
for p in &priorities {
*counts.entry(p.into()).or_insert(0) += 1;
}
}Collections of &'static str from IntoStaticStr avoid allocation overhead.
Pattern Matching with Static Strings
use strum::IntoStaticStr;
#[derive(IntoStaticStr, Clone, Copy)]
enum Color {
Red,
Green,
Blue,
}
fn match_static_str() {
let color = Color::Red;
let name: &'static str = color.into();
// &'static str can be matched on efficiently
match name {
"Red" => println!("Ruby"),
"Green" => println!("Emerald"),
"Blue" => println!("Sapphire"),
_ => unreachable!(),
}
// This enables string-based dispatch
fn process_by_name(name: &'static str) -> &'static str {
match name {
"Red" => "warm",
"Green" => "cool",
"Blue" => "cool",
_ => "unknown",
}
}
// &'static str works well with static analysis
let colors: [&'static str; 3] = ["Red", "Green", "Blue"];
for c in colors {
println!("{} is {}", c, process_by_name(c));
}
}&'static str enables efficient string matching and dispatch patterns.
Interoperability Considerations
use strum::{Display, IntoStaticStr};
#[derive(Display, IntoStaticStr)]
enum Status {
Active,
Inactive,
}
fn api_compatibility() {
let status = Status::Active;
// Some APIs require String
fn json_api(body: String) { /* ... */ }
json_api(status.to_string()); // Display works
// Some APIs accept &str
fn simple_api(s: &str) { /* ... */ }
simple_api(&status.to_string()); // Display: allocate then borrow
simple_api(status.into()); // IntoStaticStr: zero allocation
// Some APIs specifically need &'static str
fn const_api(s: &'static str) { /* ... */ }
const_api(status.into()); // IntoStaticStr works, Display doesn't
// Display::to_string() doesn't work for &'static str requirements:
// const_api(&status.to_string()); // ERROR: String doesn't live long enough
// The performance vs compatibility tradeoff:
// - IntoStaticStr: faster, but limited to static strings
// - Display: flexible, but allocates
}IntoStaticStr is required when APIs need &'static str; Display is more generally compatible.
Practical Recommendations
use strum::{Display, IntoStaticStr};
#[derive(Display, IntoStaticStr)]
enum LogLevel {
Error,
Warn,
Info,
Debug,
Trace,
}
// Recommendation: Use IntoStaticStr for:
// - Logging (no allocation overhead)
// - HTTP headers (static strings)
// - Configuration keys (static strings)
// - Error codes (static strings)
// - Event names (static strings)
// Recommendation: Use Display for:
// - User-facing messages
// - Custom formatted strings
// - API serialization
// - String concatenation/joining
// - Storage in collections that need owned strings
fn recommended_patterns() {
// Logging: IntoStaticStr is ideal
fn log(level: LogLevel, message: &str) {
println!("[{}] {}", level.into(), message); // Zero allocation for level
}
// User messages: Display is appropriate
fn user_message(level: LogLevel) -> String {
format!("Current level: {}", level) // Needs formatting
}
// Performance-critical paths: IntoStaticStr
fn hot_path(levels: &[LogLevel]) -> Vec<&'static str> {
levels.iter().map(|l| (*l).into()).collect() // Zero allocation
}
// JSON serialization: Display or IntoStaticStr both work
fn to_json(level: LogLevel) -> String {
format!(r#"{{"level": "{}"}}"#, level.into()) // IntoStaticStr works
// or: format!(r#"{{"level": "{}"}}"#, level) // Display also works
}
}Choose IntoStaticStr for performance-critical paths and APIs requiring static strings; choose Display for flexibility and formatting needs.
Synthesis
Quick reference:
use strum::{Display, IntoStaticStr};
#[derive(Display, IntoStaticStr)]
enum Example {
Variant,
}
fn comparison() {
// βββββββββββββββββββββββ¬ββββββββββββββββββββββββββββββββββββββββββββββ
// β Aspect β Display β IntoStaticStr β
// βββββββββββββββββββββββΌββββββββββββββββββββββββββββββββββββββββββββββ€
// β Returns β String (owned) β &'static str β
// β Allocation β Yes (heap allocation) β No (zero alloc) β
// β Trait β std::fmt::Display β Into<&'static str>β
// β Performance β ~50ns per call β ~1ns per call β
// β Flexibility β High (custom format) β Limited (static) β
// β Use with format! β Yes β Yes β
// β Use as &'static str β No (lifetime issue) β Yes β
// β Custom attributes β strum(to_string = "...")β Same support β
// βββββββββββββββββββββββ΄ββββββββββββββββββββββββββββββββββββββββββββββ
}
// When to use Display:
// β
Need String for storage or manipulation
// β
Working with format! macros extensively
// β
Need custom formatted strings
// β
API requires owned String
// β
String concatenation or joining
// When to use IntoStaticStr:
// β
Performance-critical code (zero allocation)
// β
Need &'static str for APIs
// β
Using as HashMap keys (no allocation)
// β
Logging in hot paths
// β
Working with static string constants
// Both support:
// β
strum(to_string = "...") attributes
// β
strum(serialize_all = "...") case transforms
// Common pattern: derive both
#[derive(Display, IntoStaticStr, Clone, Copy)]
enum Dual {
First,
Second,
}
fn best_of_both(d: Dual) {
// Use IntoStaticStr when you can
let static_str: &'static str = d.into();
// Use Display when you need String
let string: String = d.to_string();
}Key insight: The trade-off between Display and IntoStaticStr is fundamentally about allocation versus static lifetime guarantees. Display produces owned String values through the standard std::fmt::Display trait, integrating with Rust's formatting ecosystem but requiring heap allocation for each conversion. IntoStaticStr produces &'static str references through the Into trait, offering zero-allocation conversion at the cost of being limited to the variant name (or custom static string) known at compile time. In practice, IntoStaticStr is ideal for performance-sensitive code like logging, HTTP headers, and configuration keys where static strings suffice, while Display is better for user-facing messages, custom formatting, and APIs that require owned strings. Many enums benefit from deriving both traits, allowing developers to choose based on the specific contextβ.into() for static strings when possible, .to_string() when String is needed.
