Loading pageā¦
Rust walkthroughs
Loading pageā¦
once_cell::sync::OnceCell::get_or_try_init and get_or_init for fallible initialization?once_cell::sync::OnceCell::get_or_init and get_or_try_init provide different approaches to lazy initialization, with get_or_init designed for infallible initialization that always succeeds, while get_or_try_init handles fallible initialization that can fail. The key trade-off is that get_or_init requires the initialization closure to return a value directly (panicking on failure), while get_or_try_init returns a Result that can propagate errors to the caller. This distinction affects error handling strategies: get_or_init is simpler but offers no recovery mechanism for initialization failures, whereas get_or_try_init enables graceful error handling and retry semantics at the cost of additional complexity.
use once_cell::sync::OnceCell;
fn main() {
// OnceCell is a cell that can be written to only once
static INSTANCE: OnceCell<String> = OnceCell::new();
// get_or_init: infallible initialization
let value = INSTANCE.get_or_init(|| {
"Hello, world!".to_string()
});
println!("Value: {}", value);
// Subsequent calls return the same value
let same_value = INSTANCE.get_or_init(|| {
"This won't run".to_string()
});
assert!(std::ptr::eq(value, same_value));
}OnceCell holds a value that is initialized exactly once on first access.
use once_cell::sync::OnceCell;
use std::collections::HashMap;
static CONFIG: OnceCell<HashMap<&'static str, &'static str>> = OnceCell::new();
fn get_config() -> &'static HashMap<&'static str, &'static str> {
CONFIG.get_or_init(|| {
let mut map = HashMap::new();
map.insert("host", "localhost");
map.insert("port", "8080");
map.insert("database", "mydb");
map
})
}
fn main() {
let config = get_config();
println!("Host: {}", config.get("host").unwrap());
// get_or_init always succeeds (returns &T)
// The closure must return T, not Result<T, E>
}get_or_init accepts a closure that returns a value, panicking if the closure panics.
use once_cell::sync::OnceCell;
use std::fs::read_to_string;
static FILE_CONTENTS: OnceCell<String> = OnceCell::new();
fn get_file_contents() -> Result<&'static String, std::io::Error> {
FILE_CONTENTS.get_or_try_init(|| {
read_to_string("/etc/hostname")
})
}
fn main() {
match get_file_contents() {
Ok(contents) => println!("Hostname: {}", contents.trim()),
Err(e) => println!("Error reading hostname: {}", e),
}
// get_or_try_init returns Result<&T, E>
// Allows handling initialization errors gracefully
}get_or_try_init accepts a closure returning Result<T, E>, propagating errors to the caller.
use once_cell::sync::OnceCell;
static VALUE: OnceCell<i32> = OnceCell::new();
// Approach 1: get_or_init with panic on error
fn get_value_panic() -> &'static i32 {
VALUE.get_or_init(|| {
// Simulate fallible operation
read_config_value().expect("Failed to read config")
})
}
// Approach 2: get_or_try_init with error propagation
fn get_value_try() -> Result<&'static i32, String> {
static VALUE_TRY: OnceCell<i32> = OnceCell::new();
VALUE_TRY.get_or_try_init(|| {
read_config_value().ok_or("Failed to read config".to_string())
})
}
fn read_config_value() -> Option<i32> {
Some(42) // Simulated config read
}
fn main() {
// get_or_init: panic on failure
let value1 = get_value_panic();
println!("Value (panic version): {}", value1);
// get_or_try_init: return Result on failure
match get_value_try() {
Ok(value) => println!("Value (try version): {}", value),
Err(e) => println!("Error: {}", e),
}
}get_or_init requires handling errors inside the closure; get_or_try_init exposes errors to callers.
use once_cell::sync::OnceCell;
static FALLIBLE: OnceCell<String> = OnceCell::new();
fn main() {
// First call: initialization fails
let result1 = FALLIBLE.get_or_try_init(|| {
Err("Initialization failed".to_string())
});
println!("First call: {:?}", result1);
// Second call: retry initialization
let result2 = FALLIBLE.get_or_try_init(|| {
Ok("Success on retry".to_string())
});
println!("Second call: {:?}", result2);
// Third call: already initialized
let result3 = FALLIBLE.get_or_try_init(|| {
Err("This won't run".to_string())
});
println!("Third call: {:?}", result3);
// get_or_try_init allows retry on failure
// Once successful, subsequent calls return the cached value
}Failed initialization can be retried; successful initialization is permanent.
use once_cell::sync::OnceCell;
use std::sync::atomic::{AtomicUsize, Ordering};
static ATTEMPTS: AtomicUsize = AtomicUsize::new(0);
static RETRY_CELL: OnceCell<String> = OnceCell::new();
fn try_init() -> Result<String, &'static str> {
let attempts = ATTEMPTS.fetch_add(1, Ordering::SeqCst);
if attempts < 2 {
Err("Not ready yet")
} else {
Ok("Finally initialized".to_string())
}
}
fn main() {
// First attempt fails
let r1 = RETRY_CELL.get_or_try_init(try_init);
println!("Attempt 1: {:?}", r1);
// Second attempt fails
let r2 = RETRY_CELL.get_or_try_init(try_init);
println!("Attempt 2: {:?}", r2);
// Third attempt succeeds
let r3 = RETRY_CELL.get_or_try_init(try_init);
println!("Attempt 3: {:?}", r3);
// Subsequent calls return cached value
let r4 = RETRY_CELL.get_or_try_init(try_init);
println!("Attempt 4: {:?}", r4);
}Each failed call allows retry; success permanently caches the value.
use once_cell::sync::OnceCell;
use std::thread;
use std::sync::Arc;
fn main() {
static CELL: OnceCell<i32> = OnceCell::new();
let handles: Vec<_> = (0..10)
.map(|i| {
thread::spawn(move || {
let result = CELL.get_or_try_init(|| {
if i == 0 {
// First thread fails
Err("First thread failed")
} else {
Ok(42)
}
});
(i, result.is_ok())
})
})
.collect();
for handle in handles {
let (id, success) = handle.join().unwrap();
println!("Thread {}: success={}", id, success);
}
// Only one initialization attempt at a time
// Failure allows subsequent attempts
// Success blocks until complete
}get_or_try_init is thread-safe; only one initialization runs at a time.
use once_cell::sync::OnceCell;
use std::collections::HashMap;
// Use case 1: Computed constants
static FACTORIALS: OnceCell<HashMap<u32, u64>> = OnceCell::new();
fn get_factorial(n: u32) -> &'static u64 {
FACTORIALS.get_or_init(|| {
let mut map = HashMap::new();
let mut result = 1u64;
map.insert(0, 1);
for i in 1..=20 {
result *= i as u64;
map.insert(i, result);
}
map
}).get(&n).unwrap()
}
// Use case 2: Regex compilation (always succeeds)
static EMAIL_REGEX: OnceCell<regex::Regex> = OnceCell::new();
fn get_email_regex() -> &'static regex::Regex {
EMAIL_REGEX.get_or_init(|| {
regex::Regex::new(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$")
.expect("Invalid regex pattern")
})
}
// Use case 3: Static configuration
static APP_CONFIG: OnceCell<AppConfig> = OnceCell::new();
struct AppConfig {
name: &'static str,
version: &'static str,
}
fn get_app_config() -> &'static AppConfig {
APP_CONFIG.get_or_init(|| AppConfig {
name: "MyApp",
version: "1.0.0",
})
}
fn main() {
println!("5! = {}", get_factorial(5));
}Use get_or_init when initialization is guaranteed to succeed or should panic on failure.
use once_cell::sync::OnceCell;
use std::fs;
use std::net::SocketAddr;
// Use case 1: File I/O
static SECRET_KEY: OnceCell<Vec<u8>> = OnceCell::new();
fn get_secret_key() -> Result<&'static Vec<u8>, std::io::Error> {
SECRET_KEY.get_or_try_init(|| {
fs::read("/etc/secret/key")
})
}
// Use case 2: Network binding
static BIND_ADDRESS: OnceCell<SocketAddr> = OnceCell::new();
fn parse_bind_address() -> Result<&'static SocketAddr, String> {
BIND_ADDRESS.get_or_try_init(|| {
let addr: SocketAddr = std::env::var("BIND_ADDR")
.or(Ok("0.0.0.0:8080".to_string()))
.map_err(|_| "Invalid BIND_ADDR")?
.parse()
.map_err(|e| format!("Invalid address: {}", e))?;
Ok(addr)
})
}
// Use case 3: Database connection
static DB_CONNECTION: OnceCell<DbConnection> = OnceCell::new();
struct DbConnection;
impl DbConnection {
fn connect(url: &str) -> Result<Self, String> {
// Simulate connection
if url.starts_with("postgres://") {
Ok(DbConnection)
} else {
Err("Invalid connection URL".to_string())
}
}
}
fn get_db_connection() -> Result<&'static DbConnection, String> {
DB_CONNECTION.get_or_try_init(|| {
let url = std::env::var("DATABASE_URL")
.unwrap_or_else(|_| "postgres://localhost/mydb".to_string());
DbConnection::connect(&url)
})
}
fn main() {
match get_secret_key() {
Ok(key) => println!("Key length: {}", key.len()),
Err(e) => println!("Failed to load key: {}", e),
}
}Use get_or_try_init when initialization may fail and errors should be handled.
use once_cell::sync::OnceCell;
static DATA: OnceCell<String> = OnceCell::new();
fn main() {
// Option 1: get_or_try_init with .ok().unwrap() for infallible
let value = DATA.get_or_try_init(|| Ok("data".to_string()))
.expect("Initialization should never fail");
println!("Value: {}", value);
// Option 2: get_or_init with closure that handles errors
static DATA2: OnceCell<String> = OnceCell::new();
DATA2.get_or_init(|| {
fallible_operation().unwrap_or_else(|e| {
panic!("Initialization failed: {}", e)
})
});
// Option 3: get_or_try_init with proper error handling
static DATA3: OnceCell<String> = OnceCell::new();
match DATA3.get_or_try_init(|| fallible_operation()) {
Ok(value) => println!("Got: {}", value),
Err(e) => println!("Error: {}", e),
}
}
fn fallible_operation() -> Result<String, String> {
Ok("result".to_string())
}Choose based on whether callers need to handle initialization errors.
use once_cell::sync::OnceCell;
use std::time::Instant;
static COMPUTED: OnceCell<Vec<u64>> = OnceCell::new();
fn expensive_computation() -> Vec<u64> {
(0..100_000).map(|i| i * i).collect()
}
fn main() {
// First access: computation runs
let start = Instant::now();
let result = COMPUTED.get_or_init(expensive_computation);
let first_duration = start.elapsed();
println!("First access: {:?}", first_duration);
// Subsequent accesses: return cached value
let start = Instant::now();
let result = COMPUTED.get_or_init(expensive_computation);
let cached_duration = start.elapsed();
println!("Cached access: {:?}", cached_duration);
// Both get_or_init and get_or_try_init have similar performance
// after initialization; difference is in error handling overhead
}Both methods have identical performance after initialization; the difference is initialization semantics.
use once_cell::sync::OnceCell;
use std::panic;
static PANIC_CELL: OnceCell<String> = OnceCell::new();
fn main() {
// Handle panic in get_or_init
let result = panic::catch_unwind(|| {
PANIC_CELL.get_or_init(|| {
panic!("Initialization panic!");
});
});
println!("Caught panic: {}", result.is_err());
// OnceCell is NOT poisoned - can retry
let result = PANIC_CELL.get_or_init(|| {
"Recovered".to_string()
});
println!("After recovery: {}", result);
// Unlike std::sync::Once, OnceCell allows recovery from panics
}OnceCell allows retries after panic, unlike std::sync::Once which poisons permanently.
use once_cell::sync::OnceCell;
use std::thread;
use std::sync::atomic::{AtomicUsize, Ordering};
static INIT_COUNT: AtomicUsize = AtomicUsize::new(0);
static THREAD_SAFE: OnceCell<String> = OnceCell::new();
fn main() {
let handles: Vec<_> = (0..100)
.map(|_| {
thread::spawn(|| {
THREAD_SAFE.get_or_init(|| {
INIT_COUNT.fetch_add(1, Ordering::SeqCst);
"Initialized".to_string()
})
})
})
.collect();
for handle in handles {
handle.join().unwrap();
}
println!("Initialization count: {}", INIT_COUNT.load(Ordering::SeqCst));
// Always 1 - initialization happens exactly once
}Only one thread performs initialization; others wait for completion.
use once_cell::sync::Lazy;
use once_cell::sync::OnceCell;
// Lazy: initialization happens automatically on first access
static LAZY_VALUE: Lazy<String> = Lazy::new(|| {
"Initialized lazily".to_string()
});
// OnceCell: manual initialization with get_or_init
static ONCE_CELL_VALUE: OnceCell<String> = OnceCell::new();
fn get_once_cell() -> &'static String {
ONCE_CELL_VALUE.get_or_init(|| "Initialized on demand".to_string())
}
fn main() {
// Lazy automatically dereferences
println!("Lazy: {}", *LAZY_VALUE);
// OnceCell requires explicit get_or_init
println!("OnceCell: {}", get_once_cell());
// Lazy is simpler but less control over initialization timing
// OnceCell allows fallible initialization with get_or_try_init
}Lazy is simpler for infallible cases; OnceCell provides more control.
use once_cell::sync::OnceCell;
// For fallible initialization, OnceCell::get_or_try_init is the solution
// Lazy doesn't natively support fallible initialization
static FALLIBLE_CONFIG: OnceCell<Config> = OnceCell::new();
struct Config {
api_key: String,
}
fn get_config() -> Result<&'static Config, String> {
FALLIBLE_CONFIG.get_or_try_init(|| {
let api_key = std::env::var("API_KEY")
.map_err(|_| "API_KEY not set")?;
Ok(Config { api_key })
})
}
fn main() {
match get_config() {
Ok(config) => println!("API key: {}", config.api_key),
Err(e) => println!("Config error: {}", e),
}
}OnceCell with get_or_try_init is the idiomatic way to handle fallible lazy initialization.
Method comparison:
| Method | Return Type | Error Handling | Use Case |
|--------|-------------|----------------|----------|
| get_or_init | &T | Panic on failure | Infallible initialization |
| get_or_try_init | Result<&T, E> | Return Err on failure | Fallible initialization |
Key differences:
| Aspect | get_or_init | get_or_try_init |
|--------|---------------|-------------------|
| Closure return type | T | Result<T, E> |
| Error propagation | Panic | Result |
| Retry after failure | No (poisoned) | Yes |
| Caller error handling | Impossible | Possible |
When to use each:
| Scenario | Recommended Method |
|----------|-------------------|
| Computed constants | get_or_init |
| Regex compilation (fixed patterns) | get_or_init |
| File I/O | get_or_try_init |
| Network connections | get_or_try_init |
| Environment parsing | get_or_try_init |
| Database connections | get_or_try_init |
Error handling strategies:
// Strategy 1: Panic on error (get_or_init)
CELL.get_or_init(|| operation().expect("Failed"));
// Strategy 2: Propagate error (get_or_try_init)
CELL.get_or_try_init(|| operation())?;
// Strategy 3: Default value on error (get_or_try_init)
CELL.get_or_try_init(|| Ok(operation().unwrap_or(default_value)));
// Strategy 4: Log and retry later (get_or_try_init)
loop {
match CELL.get_or_try_init(|| operation()) {
Ok(value) => break value,
Err(e) => {
eprintln!("Retrying after error: {}", e);
std::thread::sleep(Duration::from_secs(1));
}
}
}Key insight: The choice between get_or_init and get_or_try_init centers on error handling strategy: get_or_init is appropriate when initialization must succeed (panicking on failure is acceptable) and simplicity is valued, while get_or_try_init is essential when initialization may legitimately fail and callers need to handle errors gracefully. get_or_try_init enables retry semanticsāfailed initialization doesn't poison the cell, allowing subsequent calls to attempt initialization again. This makes get_or_try_init suitable for transient failures (file not found, network timeout, environment variable unset) where retrying later might succeed. get_or_init should be used for invariant violations where failure indicates a bug (hardcoded regex pattern, static computation) rather than a recoverable condition.