Loading page…
Rust walkthroughs
Loading page…
The once_cell crate provides types for single-assignment cells that can be initialized once and accessed thereafter. This is essential for lazy static initialization, global configuration, and one-time setup patterns. The lazy_static crate offers a macro-based approach for the same purpose. Both solve the problem of safely initializing global or static data at runtime, which is otherwise difficult in Rust due to the requirement that static items have a constant initializer.
Key types:
Lazy<T> — lazily initialized value (once_cell)OnceLock<T> — thread-safe once-initialization (once_cell)lazy_static! — macro for lazy static initializationsync::Once — std type for one-time execution# Cargo.toml
[dependencies]
once_cell = "1"
# Or use lazy_static:
# lazy_static = "1"use once_cell::sync::Lazy;
// Global lazy-initialized value
static CONFIG: Lazy<Vec<String>> = Lazy::new(|| {
vec![
"setting1".to_string(),
"setting2".to_string(),
]
});
fn main() {
// First access initializes the value
println!("Config: {:?}", *CONFIG);
// Subsequent accesses reuse the initialized value
println!("Config: {:?}", *CONFIG);
}use once_cell::sync::Lazy;
use std::collections::HashMap;
// Static lazy-initialized HashMap
static WORD_COUNTS: Lazy<HashMap<&'static str, usize>> = Lazy::new(|| {
let mut map = HashMap::new();
map.insert("hello", 1);
map.insert("world", 2);
map.insert("rust", 3);
map
});
// Lazy with expensive computation
static EXPENSIVE: Lazy<Vec<u64>> = Lazy::new(|| {
println!("Computing expensive value...");
(0..10).map(|n| n * n).collect()
});
// Lazy with configuration
static APP_CONFIG: Lazy<Config> = Lazy::new(|| {
Config {
name: "MyApp".to_string(),
version: "1.0.0".to_string(),
debug: true,
}
});
#[derive(Debug)]
struct Config {
name: String,
version: String,
debug: bool,
}
fn main() {
println!("Word counts: {:?}", *WORD_COUNTS);
// EXPENSIVE is computed on first access
println!("Before first access");
println!("Expensive: {:?}", *EXPENSIVE);
println!("After first access");
// Access config
println!("Config: {:?}", *APP_CONFIG);
}use once_cell::sync::OnceLock;
use std::collections::HashMap;
// OnceLock for runtime initialization
static DATABASE: OnceLock<HashMap<String, String>> = OnceLock::new();
fn get_database() -> &'static HashMap<String, String> {
DATABASE.get_or_init(|| {
let mut db = HashMap::new();
db.insert("key1".to_string(), "value1".to_string());
db.insert("key2".to_string(), "value2".to_string());
db
})
}
// OnceLock with manual initialization
static SETTINGS: OnceLock<String> = OnceLock::new();
fn init_settings(value: String) -> Result<(), String> {
SETTINGS.set(value).map_err(|_| "Already initialized".to_string())
}
fn main() {
// Get or initialize
let db = get_database();
println!("Database: {:?}", db);
// Manual initialization
init_settings("production".to_string()).unwrap();
println!("Settings: {:?}", SETTINGS.get());
// Trying to set again fails
let result = init_settings("test".to_string());
println!("Second init: {:?}", result);
// Check if initialized
println!("Is initialized: {}", SETTINGS.get().is_some());
}
// Thread-safe initialization
fn thread_safe_example() {
use once_cell::sync::OnceLock;
use std::thread;
static CACHE: OnceLock<Vec<i32>> = OnceLock::new();
let handles: Vec<_> = (0..4)
.map(|i| {
thread::spawn(move || {
// All threads will get the same value
// Only one thread will initialize
let cache = CACHE.get_or_init(|| {
println!("Thread {} initializing", i);
(0..10).collect()
});
cache.clone()
})
})
.collect();
for handle in handles {
let result = handle.join().unwrap();
println!("Got: {:?}", result);
}
}use once_cell::unsync::Lazy;
use once_cell::unsync::OnceCell;
fn main() {
// Unsync Lazy - not thread-safe, but faster
let lazy: Lazy<Vec<i32>> = Lazy::new(|| {
println!("Initializing lazy value");
vec![1, 2, 3]
});
println!("Before access");
println!("Value: {:?}", *lazy);
println!("After access");
// OnceCell - single assignment cell
let cell: OnceCell<String> = OnceCell::new();
println!("Is set: {}", cell.get().is_some());
// Set the value
cell.set("Hello".to_string()).unwrap();
println!("Is set: {}", cell.get().is_some());
println!("Value: {:?}", cell.get());
// Cannot set again
let result = cell.set("World".to_string());
println!("Second set: {:?}", result);
// get_or_init
let cell2: OnceCell<Vec<i32>> = OnceCell::new();
let value = cell2.get_or_init(|| vec![1, 2, 3]);
println!("Value: {:?}", value);
}
// Using in structs
struct Cache {
data: OnceCell<Vec<String>>,
}
impl Cache {
fn new() -> Self {
Self {
data: OnceCell::new(),
}
}
fn get_data(&self) -> &Vec<String> {
self.data.get_or_init(|| {
// Expensive computation
vec!["item1".to_string(), "item2".to_string()]
})
}
}
fn cache_example() {
let cache = Cache::new();
println!("Data: {:?}", cache.get_data());
}# Cargo.toml
[dependencies]
lazy_static = "1"#[macro_use]
extern crate lazy_static;
use std::collections::HashMap;
lazy_static! {
static ref CONFIG: HashMap<&'static str, &'static str> = {
let mut m = HashMap::new();
m.insert("host", "localhost");
m.insert("port", "8080");
m.insert("debug", "true");
m
};
static ref NUMBERS: Vec<i32> = {
(1..=10).collect()
};
static ref GREETING: String = {
"Hello, World!".to_string()
};
}
fn main() {
println!("Config: {:?}", *CONFIG);
println!("Numbers: {:?}", *NUMBERS);
println!("Greeting: {}", *GREETING);
// Access like regular static
if let Some(host) = CONFIG.get("host") {
println!("Host: {}", host);
}
}// once_cell - type-based approach
use once_cell::sync::Lazy;
static VALUE1: Lazy<Vec<i32>> = Lazy::new(|| vec![1, 2, 3]);
// lazy_static - macro-based approach
#[macro_use]
extern crate lazy_static;
lazy_static! {
static ref VALUE2: Vec<i32> = vec![1, 2, 3];
}
fn main() {
// Both work similarly
println!("once_cell: {:?}", *VALUE1);
println!("lazy_static: {:?}", *VALUE2);
}
// Key differences:
// 1. once_cell supports generic types naturally
// 2. once_cell works without macros
// 3. once_cell has both sync and unsync variants
// 4. lazy_static uses a macro, which can be more verbose
// 5. once_cell is now part of std (Rust 1.70+)use once_cell::sync::{Lazy, OnceLock};
use std::collections::HashMap;
use std::sync::Mutex;
// Pattern 1: Global configuration
static CONFIG: Lazy<Config> = Lazy::new(|| {
Config::from_env().unwrap_or_default()
});
#[derive(Debug)]
struct Config {
database_url: String,
max_connections: usize,
}
impl Config {
fn from_env() -> Option<Self> {
Some(Self {
database_url: std::env::var("DATABASE_URL").unwrap_or_else(|_| "localhost".to_string()),
max_connections: std::env::var("MAX_CONNECTIONS")
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(10),
})
}
}
impl Default for Config {
fn default() -> Self {
Self {
database_url: "localhost".to_string(),
max_connections: 10,
}
}
}
// Pattern 2: Regex caching
static EMAIL_REGEX: Lazy<regex::Regex> = Lazy::new(|| {
regex::Regex::new(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$").unwrap()
});
fn is_valid_email(email: &str) -> bool {
EMAIL_REGEX.is_match(email)
}
// Pattern 3: Singleton pattern
static INSTANCE: OnceLock<Singleton> = OnceLock::new();
#[derive(Debug)]
struct Singleton {
id: usize,
}
impl Singleton {
fn get() -> &'static Singleton {
INSTANCE.get_or_init(|| Singleton { id: 1 })
}
}
// Pattern 4: Thread-safe global state
static COUNTER: Lazy<Mutex<i32>> = Lazy::new(|| Mutex::new(0));
fn increment_counter() -> i32 {
let mut count = COUNTER.lock().unwrap();
*count += 1;
*count
}
fn main() {
println!("Config: {:?}", *CONFIG);
println!("Email valid: {}", is_valid_email("test@example.com"));
let singleton = Singleton::get();
println!("Singleton: {:?}", singleton);
println!("Counter: {}", increment_counter());
println!("Counter: {}", increment_counter());
}use once_cell::sync::Lazy;
// Order matters - dependencies must be initialized first
static BASE_URL: Lazy<String> = Lazy::new(|| {
std::env::var("BASE_URL").unwrap_or_else(|_| "https://api.example.com".to_string())
});
static API_CLIENT: Lazy<ApiClient> = Lazy::new(|| {
ApiClient::new(&BASE_URL)
});
#[derive(Debug)]
struct ApiClient {
base_url: String,
}
impl ApiClient {
fn new(base_url: &str) -> Self {
Self {
base_url: base_url.to_string(),
}
}
fn request(&self, endpoint: &str) -> String {
format!("{}{}", self.base_url, endpoint)
}
}
fn main() {
// BASE_URL is initialized first, then API_CLIENT
println!("API Client: {:?}", *API_CLIENT);
println!("Request: {}", API_CLIENT.request("/users"));
}
// Circular dependencies are a problem
// This would cause a deadlock:
// static A: Lazy<String> = Lazy::new(|| format!("{}", *B));
// static B: Lazy<String> = Lazy::new(|| format!("{}", *A));use once_cell::sync::{Lazy, OnceLock};
use std::sync::OnceLock as StdOnceLock;
// Option 1: Use Lazy with Result (panic on error)
static CONFIG: Lazy<Config> = Lazy::new(|| {
Config::load().expect("Failed to load config")
});
// Option 2: Use OnceLock with Option
static OPTIONAL_CONFIG: OnceLock<Option<Config>> = OnceLock::new();
fn get_config() -> Option<&'static Config> {
OPTIONAL_CONFIG.get_or_init(|| Config::load().ok()).as_ref()
}
// Option 3: Use OnceLock with Result stored
static RESULT_CONFIG: OnceLock<Result<Config, String>> = OnceLock::new();
fn init_config() -> &'static Result<Config, String> {
RESULT_CONFIG.get_or_init(|| Config::load().map_err(|e| e.to_string()))
}
#[derive(Debug, Clone)]
struct Config {
name: String,
}
impl Config {
fn load() -> Result<Self, std::io::Error> {
Ok(Self {
name: "my-app".to_string(),
})
}
}
fn main() {
// Panic on error
println!("Config: {:?}", *CONFIG);
// Handle Option
match get_config() {
Some(config) => println!("Got config: {:?}", config),
None => println!("No config available"),
}
// Handle Result
match init_config() {
Ok(config) => println!("Config: {:?}", config),
Err(e) => println!("Error: {}", e),
}
}use once_cell::sync::{Lazy, OnceLock};
use std::thread;
static GLOBAL_DATA: Lazy<Vec<i32>> = Lazy::new(|| {
println!("Initializing GLOBAL_DATA");
(0..5).collect()
});
static ONCE_VALUE: OnceLock<String> = OnceLock::new();
fn main() {
// Spawn multiple threads that all access the same lazy value
let handles: Vec<_> = (0..5)
.map(|i| {
thread::spawn(move || {
// First thread to access initializes
let data = &*GLOBAL_DATA;
println!("Thread {} got: {:?}", i, data);
})
})
.collect();
for handle in handles {
handle.join().unwrap();
}
// OnceLock with threads
let handles: Vec<_> = (0..3)
.map(|i| {
thread::spawn(move || {
// get_or_init is thread-safe
let value = ONCE_VALUE.get_or_init(|| {
println!("Thread {} initializing", i);
format!("Initialized by thread {}", i)
});
println!("Thread {} got: {}", i, value);
})
})
.collect();
for handle in handles {
handle.join().unwrap();
}
}
// Mutex for mutable global state
use std::sync::Mutex;
static GLOBAL_STATE: Lazy<Mutex<GlobalState>> = Lazy::new(|| {
Mutex::new(GlobalState {
count: 0,
items: Vec::new(),
})
});
struct GlobalState {
count: u32,
items: Vec<String>,
}
fn modify_state() {
let mut state = GLOBAL_STATE.lock().unwrap();
state.count += 1;
state.items.push(format!("Item {}", state.count));
}
fn read_state() -> (u32, usize) {
let state = GLOBAL_STATE.lock().unwrap();
(state.count, state.items.len())
}use once_cell::unsync::OnceCell;
struct ExpensiveInitializer {
cache: OnceCell<Vec<String>>,
}
impl ExpensiveInitializer {
fn new() -> Self {
Self {
cache: OnceCell::new(),
}
}
fn get_data(&self) -> &Vec<String> {
self.cache.get_or_init(|| {
println!("Computing expensive data...");
(0..10).map(|i| format!("Item {}", i)).collect()
})
}
fn is_initialized(&self) -> bool {
self.cache.get().is_some()
}
}
fn main() {
let init = ExpensiveInitializer::new();
println!("Initialized: {}", init.is_initialized());
let data = init.get_data();
println!("Data: {:?}", data);
println!("Initialized: {}", init.is_initialized());
// Second call returns cached value
let data2 = init.get_data();
println!("Same reference: {}", std::ptr::eq(data, data2));
}
// Optional initialization
struct OptionalCache {
data: OnceCell<Vec<i32>>,
}
impl OptionalCache {
fn new() -> Self {
Self {
data: OnceCell::new(),
}
}
fn set(&self, data: Vec<i32>) -> Result<(), Vec<i32>> {
self.data.set(data)
}
fn get(&self) -> Option<&Vec<i32>> {
self.data.get()
}
fn get_or_default(&self) -> &Vec<i32> {
self.data.get_or_init(Vec::new)
}
}use once_cell::sync::Lazy;
use std::sync::Mutex;
#[derive(Debug, Clone)]
struct Connection {
id: usize,
}
struct ConnectionPool {
connections: Vec<Connection>,
next_id: usize,
}
impl ConnectionPool {
fn new(size: usize) -> Self {
Self {
connections: (0..size).map(|i| Connection { id: i }).collect(),
next_id: size,
}
}
fn get(&mut self) -> Option<Connection> {
self.connections.pop()
}
fn put(&mut self, conn: Connection) {
self.connections.push(conn);
}
}
// Global connection pool
static POOL: Lazy<Mutex<ConnectionPool>> = Lazy::new(|| {
println!("Initializing connection pool");
Mutex::new(ConnectionPool::new(5))
});
fn get_connection() -> Option<Connection> {
let mut pool = POOL.lock().unwrap();
pool.get()
}
fn return_connection(conn: Connection) {
let mut pool = POOL.lock().unwrap();
pool.put(conn);
}
fn main() {
// Pool is initialized on first access
let conn1 = get_connection().expect("No connection available");
println!("Got connection: {}", conn1.id);
let conn2 = get_connection().expect("No connection available");
println!("Got connection: {}", conn2.id);
// Return connections
return_connection(conn1);
return_connection(conn2);
println!("Connections returned");
}use once_cell::sync::Lazy;
use std::collections::HashMap;
use std::sync::Mutex;
// Simple regex (pseudo-code - would use regex crate)
type Regex = String;
fn compile_regex(pattern: &str) -> Result<Regex, String> {
// In real code, this would compile the regex
Ok(pattern.to_string())
}
// Cache compiled regexes
static REGEX_CACHE: Lazy<Mutex<HashMap<String, Regex>>> = Lazy::new(|| {
Mutex::new(HashMap::new())
});
fn get_regex(pattern: &str) -> Result<Regex, String> {
// Check cache first
{
let cache = REGEX_CACHE.lock().unwrap();
if let Some(regex) = cache.get(pattern) {
return Ok(regex.clone());
}
}
// Compile and cache
let regex = compile_regex(pattern)?;
{
let mut cache = REGEX_CACHE.lock().unwrap();
cache.insert(pattern.to_string(), regex.clone());
}
Ok(regex)
}
// Pre-compiled common patterns
static EMAIL_PATTERN: Lazy<Regex> = Lazy::new(|| {
compile_regex(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$")
.expect("Invalid email regex")
});
static PHONE_PATTERN: Lazy<Regex> = Lazy::new(|| {
compile_regex(r"^\d{3}-\d{3}-\d{4}$")
.expect("Invalid phone regex")
});
fn main() {
// Use pre-compiled patterns
println!("Email pattern: {}", *EMAIL_PATTERN);
println!("Phone pattern: {}", *PHONE_PATTERN);
// Use cached patterns
let pattern1 = get_regex(r"\d+").unwrap();
let pattern2 = get_regex(r"\d+").unwrap(); // From cache
println!("Pattern: {}", pattern1);
}use std::sync::OnceLock;
static GLOBAL_VALUE: OnceLock<String> = OnceLock::new();
fn main() {
// std::sync::OnceLock is the standard library version
// (once_cell is being integrated into std)
// Get or initialize
let value = GLOBAL_VALUE.get_or_init(|| {
println!("Initializing value");
"Hello, World!".to_string()
});
println!("Value: {}", value);
// Check if initialized
println!("Is set: {}", GLOBAL_VALUE.get().is_some());
// Manual set
static MANUAL: OnceLock<i32> = OnceLock::new();
MANUAL.set(42).unwrap();
println!("Manual: {:?}", MANUAL.get());
// into_inner (takes value, requires ownership)
// Only available if you have ownership of the OnceLock
let local_once: OnceLock<Vec<i32>> = OnceLock::new();
local_once.set(vec![1, 2, 3]).unwrap();
let inner = local_once.into_inner();
println!("Inner: {:?}", inner);
}// Before: lazy_static
#[macro_use]
extern crate lazy_static;
lazy_static! {
static ref CONFIG: HashMap<&'static str, String> = {
let mut m = HashMap::new();
m.insert("host", "localhost".to_string());
m.insert("port", "8080".to_string());
m
};
static ref COUNT: i32 = 42;
}
// After: once_cell
use once_cell::sync::Lazy;
use std::collections::HashMap;
static CONFIG: Lazy<HashMap<&'static str, String>> = Lazy::new(|| {
let mut m = HashMap::new();
m.insert("host", "localhost".to_string());
m.insert("port", "8080".to_string());
m
});
static COUNT: Lazy<i32> = Lazy::new(|| 42);
// Migration is straightforward:
// lazy_static! {
// static ref NAME: Type = value;
// }
//
// becomes:
// static NAME: Lazy<Type> = Lazy::new(|| value);once_cell::sync::Lazy for lazy static initializationonce_cell::sync::OnceLock for runtime one-time initializationonce_cell::unsync::Lazy for single-threaded lazy values (faster)once_cell::unsync::OnceCell for single-threaded single-assignment cellslazy_static! macro is an alternative, but once_cell is more idiomatic in modern Ruststd::sync::OnceLock is available in Rust 1.70+ (standard library version)Mutex for mutable global stateOption or Result stored in the cell