Loading pageā¦
Rust walkthroughs
Loading pageā¦
lazy_static::lazy_static! differ from once_cell::sync::Lazy for lazy static initialization patterns?lazy_static::lazy_static! and once_cell::sync::Lazy both provide lazy initialization of static values, but they differ significantly in API design, ergonomics, and integration with Rust's type system. The lazy_static! macro uses a code-generation approach that creates hidden structs implementing Deref, while once_cell::Lazy<T> is a direct type that holds an initialization closure and value, providing more idiomatic Rust integration, better error messages, and simpler access patterns. The once_cell approach is now part of the standard library (as std::sync::OnceLock and std::sync::LazyLock in recent versions), making lazy_static largely obsolete for new code.
use lazy_static::lazy_static;
use std::sync::Mutex;
lazy_static! {
static ref CONFIG: Mutex<Config> = Mutex::new(Config::default());
static ref CACHE: Mutex<Vec<String>> = Mutex::new(Vec::new());
}
#[derive(Default)]
struct Config {
debug: bool,
port: u16,
}
fn main() {
// Access via deref coercion
CONFIG.lock().unwrap().debug = true;
CACHE.lock().unwrap().push("entry".to_string());
// The static is initialized on first access
// Subsequent accesses use the cached value
}lazy_static! uses a macro to generate hidden structs that implement Deref for lazy access.
use once_cell::sync::Lazy;
use std::sync::Mutex;
static CONFIG: Lazy<Mutex<Config>> = Lazy::new(|| Mutex::new(Config::default()));
static CACHE: Lazy<Mutex<Vec<String>>> = Lazy::new(|| Mutex::new(Vec::new()));
#[derive(Default)]
struct Config {
debug: bool,
port: u16,
}
fn main() {
// Access is direct, no special syntax
CONFIG.lock().unwrap().debug = true;
CACHE.lock().unwrap().push("entry".to_string());
// Lazy<T> implements Deref<Target = T>
// First access initializes the value
}once_cell::Lazy uses a regular static declaration with a type that handles lazy initialization.
use lazy_static::lazy_static;
use once_cell::sync::Lazy;
// lazy_static: macro-based syntax
lazy_static! {
static ref VALUE1: String = String::from("hello");
static ref VALUE2: Vec<i32> = {
let mut v = Vec::new();
v.push(1);
v.push(2);
v
};
}
// once_cell: type-based syntax
static VALUE3: Lazy<String> = Lazy::new(|| String::from("hello"));
static VALUE4: Lazy<Vec<i32>> = Lazy::new(|| {
let mut v = Vec::new();
v.push(1);
v.push(2);
v
});
fn main() {
// Both work the same way at runtime
println!("lazy_static: {}", *VALUE1);
println!("once_cell: {}", *VALUE3);
}lazy_static uses a macro block with static ref; once_cell::Lazy uses normal static declarations.
use lazy_static::lazy_static;
use once_cell::sync::Lazy;
// lazy_static: hidden type, accessed via Deref
lazy_static! {
static ref LAZY_STATIC_VALUE: i32 = 42;
}
// once_cell: explicit type Lazy<i32>
static ONCE_CELL_VALUE: Lazy<i32> = Lazy::new(|| 42);
fn main() {
// lazy_static: type is opaque
// The macro creates something like:
// struct LAZY_STATIC_VALUE { ... }
// impl Deref for LAZY_STATIC_VALUE { ... }
// once_cell: type is Lazy<i32>
// You can see and name the type
fn use_lazy_static(value: &i32) {
println!("Value: {}", value);
}
use_lazy_static(&ONCE_CELL_VALUE);
// With lazy_static, you'd need:
fn use_lazy_static_macro(value: &i32) {
println!("Value: {}", value);
}
// The Deref happens automatically
use_lazy_static_macro(&LAZY_STATIC_VALUE);
}once_cell::Lazy exposes the actual type; lazy_static hides it behind a macro-generated struct.
use lazy_static::lazy_static;
use once_cell::sync::Lazy;
lazy_static! {
static ref LAZY_VEC: Vec<i32> = vec
![1, 2, 3];
}
static ONCE_VEC: Lazy<Vec<i32>> = Lazy::new(|| vec
![1, 2, 3]);
fn main() {
// Both implement Deref to the inner type
// lazy_static: deref coercion
let len = LAZY_VEC.len()
; // Deref to Vec<i32>
let first = LAZY_VEC[0]; // Index uses Deref
// once_cell: same deref coercion
let len = ONCE_VEC.len();
let first = ONCE_VEC[0];
// Explicit deref works for both
let vec_ref: &Vec<i32> = &*LAZY_VEC;
let vec_ref: &Vec<i32> = &*ONCE_VEC;
println!("lazy_static len: {}", len);
println!("once_cell len: {}", len);
}Both use Deref to provide transparent access to the inner value.
use once_cell::sync::Lazy;
// once_cell::Lazy takes a closure for initialization
static DATABASE: Lazy<Database> = Lazy::new(|| {
// Complex initialization
let db = Database::connect("localhost:5432")
.expect("Failed to connect");
db.run_migrations();
db
});
static COMPUTED: Lazy<String> = Lazy::new(|| {
// Expensive computation
let mut result = String::new();
for i in 0..100 {
result.push_str(&format!("{}-", i));
}
result
});
struct Database {
url: String,
}
impl Database {
fn connect(url: &str) -> Result<Self, String> {
Ok(Database { url: url.to_string() })
}
fn run_migrations(&self) {
// Migration logic
}
}
fn main() {
// Initialization happens on first access
// The closure is called exactly once
println!("Database: {}", DATABASE.url);
println!("Computed: {}", COMPUTED.chars().take(10).collect::<String>());
}Lazy::new takes a FnOnce closure that initializes the value on first access.
use once_cell::sync::Lazy;
use std::sync::OnceLock;
// Lazy cannot handle initialization errors
// If the closure panics, Lazy is poisoned
// Use OnceLock for fallible initialization
static MAYBE_DATABASE: OnceLock<Database> = OnceLock::new();
struct Database {
connected: bool,
}
impl Database {
fn connect() -> Result<Self, String> {
// Simulated connection
Ok(Database { connected: true })
}
}
fn get_database() -> &'static Database {
MAYBE_DATABASE.get_or_init(|| {
// This closure returns the value, not Result
Database::connect().expect("Database connection failed")
})
}
// For fallible initialization, use get_or_try_init
fn get_database_fallible() -> Result<&'static Database, String> {
static DB: OnceLock<Database> = OnceLock::new();
DB.get_or_try_init(|| Database::connect())
}
fn main() {
let db = get_database();
println!("Database connected: {}", db.connected);
}For fallible initialization, use OnceLock::get_or_try_init instead of Lazy.
use lazy_static::lazy_static;
use once_cell::sync::Lazy;
use std::sync::Mutex;
use std::thread;
lazy_static! {
static ref LAZY_COUNTER: Mutex<i32> = Mutex::new(0);
}
static ONCE_COUNTER: Lazy<Mutex<i32>> = Lazy::new(|| Mutex::new(0));
fn main() {
// Both are thread-safe
// Only one thread initializes the value
// All threads see the same value
let mut handles = vec
![];
for _ in 0..10 {
let handle = thread::spawn(|| {
*LAZY_COUNTER.lock().unwrap() += 1;
*ONCE_COUNTER.lock().unwrap() += 1;
});
handles.push(handle)
;
}
for handle in handles {
handle.join().unwrap();
}
println!("lazy_static counter: {}", *LAZY_COUNTER.lock().unwrap());
println!("once_cell counter: {}", *ONCE_COUNTER.lock().unwrap());
}Both lazy_static! and once_cell::sync::Lazy provide thread-safe initialization.
use lazy_static::lazy_static;
use once_cell::sync::Lazy;
lazy_static! {
static ref LAZY_INIT: String = {
println!("lazy_static initializing");
String::from("lazy")
};
}
static ONCE_INIT: Lazy<String> = Lazy::new(|| {
println!("once_cell initializing");
String::from("once")
});
fn main() {
println!("Before first access");
// This triggers lazy_static initialization
let _ = &*LAZY_INIT;
println!("After lazy_static");
// This triggers once_cell initialization
let _ = &*ONCE_INIT;
println!("After once_cell");
// Both initialize lazily - only when first accessed
}Both approaches initialize values on first access, not at program startup.
// Rust 1.70+ includes OnceLock and LazyLock in std
// once_cell::sync::Lazy -> std::sync::LazyLock (Rust 1.80+)
// once_cell::sync::OnceLock -> std::sync::OnceLock
use std::sync::OnceLock;
static STD_LAZY: OnceLock<String> = OnceLock::new();
fn main() {
// Standard library approach (Rust 1.70+)
let value = STD_LAZY.get_or_init(|| {
String::from("standard library")
});
println!("Value: {}", value);
// OnceLock is in std::sync
// LazyLock (once_cell::Lazy equivalent) is in std::sync (Rust 1.80+)
}Modern Rust includes OnceLock and LazyLock in the standard library.
use lazy_static::lazy_static;
use once_cell::sync::Lazy;
use std::collections::HashMap;
lazy_static! {
static ref LAZY_MAP: HashMap<&'static str, i32> = {
let mut m = HashMap::new();
m.insert("one", 1);
m.insert("two", 2);
m.insert("three", 3);
m
};
}
static ONCE_MAP: Lazy<HashMap<&'static str, i32>> = Lazy::new(|| {
let mut m = HashMap::new();
m.insert("one", 1);
m.insert("two", 2);
m.insert("three", 3);
m
});
fn main() {
// Both work identically for complex types
println!("lazy_static map: {:?}", *LAZY_MAP);
println!("once_cell map: {:?}", *ONCE_MAP);
// Access elements
println!("one: {}", LAZY_MAP.get("one").unwrap());
println!("two: {}", ONCE_MAP.get("two").unwrap());
}Both handle complex types like HashMap equivalently.
use once_cell::sync::Lazy;
use std::sync::OnceLock;
// Lazy cannot be const-initialized with a non-const closure
// But OnceLock can be const-initialized with a None value
static EMPTY_VEC: OnceLock<Vec<i32>> = OnceLock::new();
// Lazy requires an initialization closure
static LAZY_VEC: Lazy<Vec<i32>> = Lazy::new(Vec::new);
fn main() {
// OnceLock::new() is const
// Lazy::new() is not const (takes a closure)
// Initialize when needed
EMPTY_VEC.get_or_init(|| vec
![1, 2, 3]);
println!("OnceLock: {:?}", EMPTY_VEC.get().unwrap())
;
println!("Lazy: {:?}", *LAZY_VEC);
}OnceLock::new() is const; Lazy::new() requires a non-const closure.
use lazy_static::lazy_static;
use once_cell::sync::Lazy;
// lazy_static: dependencies between statics work
lazy_static! {
static ref BASE_CONFIG: Config = Config::default();
static ref DERIVED_CONFIG: DerivedConfig = {
DerivedConfig::from_base(&BASE_CONFIG)
};
}
// once_cell: same dependency pattern
static BASE_CONFIG2: Lazy<Config> = Lazy::new(Config::default);
static DERIVED_CONFIG2: Lazy<DerivedConfig> = Lazy::new(|| {
DerivedConfig::from_base(&BASE_CONFIG2)
});
#[derive(Default)]
struct Config {
port: u16,
}
struct DerivedConfig {
port: u16,
host: String,
}
impl DerivedConfig {
fn from_base(base: &Config) -> Self {
DerivedConfig {
port: base.port,
host: "localhost".to_string(),
}
}
}
fn main() {
// Both handle dependencies correctly
// DERIVED_CONFIG depends on BASE_CONFIG
// Accessing DERIVED_CONFIG first initializes BASE_CONFIG
println!("Port: {}", DERIVED_CONFIG.port);
println!("Port: {}", DERIVED_CONFIG2.port);
}Both handle dependencies between lazy statics correctly.
use lazy_static::lazy_static;
use once_cell::sync::Lazy;
use std::time::Instant;
lazy_static! {
static ref LAZY_VALUE: String = {
// Simulated expensive initialization
std::thread::sleep(std::time::Duration::from_millis(10));
String::from("lazy")
};
}
static ONCE_VALUE: Lazy<String> = Lazy::new(|| {
std::thread::sleep(std::time::Duration::from_millis(10));
String::from("once")
});
fn main() {
// First access: initializes
let start = Instant::now();
let _ = &*LAZY_VALUE;
println!("lazy_static first: {:?}", start.elapsed());
let start = Instant::now();
let _ = &*ONCE_VALUE;
println!("once_cell first: {:?}", start.elapsed());
// Subsequent access: cached
let start = Instant::now();
let _ = &*LAZY_VALUE;
println!("lazy_static cached: {:?}", start.elapsed());
let start = Instant::now();
let _ = &*ONCE_VALUE;
println!("once_cell cached: {:?}", start.elapsed());
// Both have similar performance after initialization
// once_cell may be slightly faster due to simpler implementation
}Both have similar performance; once_cell may be slightly faster after initialization.
use once_cell::sync::Lazy;
// once_cell provides better error messages
// because the type is explicit
static VALUE: Lazy<String> = Lazy::new(|| String::from("hello"));
fn main() {
// Type is clearly Lazy<String>
// Compiler errors show the actual type
// Error: mismatched types
// let v: i32 = VALUE; // Error: expected i32, found Lazy<String>
// With lazy_static, the type is hidden
// Error messages may be less clear
// once_cell integrates better with IDE features
// - Go to definition works
// - Type inference works
// - Documentation is accessible
}once_cell::Lazy provides better IDE integration and error messages due to explicit typing.
// Before: lazy_static
// lazy_static! {
// static ref CONFIG: Mutex<Config> = Mutex::new(Config::default());
// }
// After: once_cell::sync::Lazy
use once_cell::sync::Lazy;
use std::sync::Mutex;
static CONFIG: Lazy<Mutex<Config>> = Lazy::new(|| Mutex::new(Config::default()));
// Or with std (Rust 1.80+)
// static CONFIG: std::sync::LazyLock<Mutex<Config>> =
// std::sync::LazyLock::new(|| Mutex::new(Config::default()));
#[derive(Default)]
struct Config {
debug: bool,
}
fn main() {
// Usage remains the same
CONFIG.lock().unwrap().debug = true;
}Migration from lazy_static to once_cell is straightforward: remove the macro and change the type.
API Comparison:
| Aspect | lazy_static! | once_cell::Lazy |
|--------|--------------|-----------------|
| Syntax | Macro block | Type annotation |
| Type visibility | Hidden | Explicit Lazy<T> |
| Initialization | Closure in macro | Lazy::new(\|\| ...) |
| Access | Deref coercion | Deref coercion |
| Thread safety | Yes | Yes |
| Std support | No | Yes (as LazyLock) |
Key Differences:
| Feature | lazy_static | once_cell | |---------|-------------|-----------| | Type clarity | Opaque | Explicit | | IDE support | Limited | Full | | Error messages | May be unclear | Clear types | | Std integration | External crate | In std (Rust 1.80+) | | Fallible init | Not supported | Use OnceLock | | Const init | No | OnceLock::new() |
Use Cases:
| Scenario | Recommended |
|----------|-------------|
| New code | std::sync::LazyLock or once_cell::sync::Lazy |
| Fallible initialization | OnceLock::get_or_try_init |
| Simple lazy value | Lazy::new(\|\| value) |
| Complex init | Lazy::new(\|\| { ... }) |
| Const context | OnceLock::new() then get_or_init |
Key insight: lazy_static::lazy_static! and once_cell::sync::Lazy achieve the same goalālazy initialization of static valuesābut differ fundamentally in approach. lazy_static! is a macro that generates hidden types implementing Deref, while once_cell::Lazy<T> is an explicit type that takes an initialization closure. This difference has practical implications: once_cell::Lazy provides better IDE support (go-to-definition, type hints), clearer compiler error messages, and composes naturally with Rust's type system. The once_cell approach has been incorporated into the standard library as std::sync::OnceLock and std::sync::LazyLock (Rust 1.70+ and 1.80+), making lazy_static largely obsolete for new code. For fallible initialization where the closure might return Result, use OnceLock::get_or_try_init which can propagate initialization errors rather than panic. The underlying implementation of both uses similar synchronization primitives (typically Once), so performance is comparable after initialization.