Null Safety
Oxide provides comprehensive null safety through optional types and safe-handling operators. Types are non-nullable by default.
Nullable Types
Use T? to create a nullable (optional) type:
let name: String? = "Alice" // Has a value
let empty: String? = null // No value
Transpiles to:
let name: Option<String> = Some("Alice".to_string());
let empty: Option<String> = None;
The null Keyword
Use null instead of None:
var value: Int? = 42
value = null
fn findUser(id: Int): User? {
if id == 0 {
return null
}
return database.getUser(id)
}
Transpiles to:
let mut value: Option<isize> = Some(42);
value = None;
fn find_user(id: isize) -> Option<User> {
if id == 0 {
return None;
}
return database.get_user(id);
}
Implicit Wrapping
Non-null values are automatically wrapped when needed:
let x: Int? = 42 // Automatically Some(42)
fn getValue(): Int? = 100 // Automatically Some(100)
Transpiles to:
let x: Option<isize> = Some(42);
fn get_value() -> Option<isize> { Some(100) }
Safe-Handling Operators
Not-Null Assertion (!!)
Unwraps the value, panicking if null:
let name: String? = "Bob"
let unwrapped: String = name!! // "Bob"
let empty: String? = null
// empty!! // Panics!
Transpiles to:
let name: Option<String> = Some("Bob".to_string());
let unwrapped: String = name.unwrap();
Only use !! when you're certain the value is not null. Prefer safer alternatives.
Null Coalescing (??)
Provides a default value when null:
let name: String? = null
let result = name ?? "Default" // "Default"
let value: Int? = 42
let result = value ?? 0 // 42
// Chain with complex defaults
let config = loadConfig() ?? Config.default()
Transpiles to:
let name: Option<String> = None;
let result = name.unwrap_or_else(|| "Default".to_string());
let value: Option<isize> = Some(42);
let result = value.unwrap_or(0);
Safe Call (?.)
Calls methods only if non-null, propagating null otherwise:
let name: String? = "hello"
let len: Int? = name?.len() // Some(5)
let empty: String? = null
let len: Int? = empty?.len() // null
// Chain safe calls
let first: Char? = name?.chars()?.next()
// Access fields safely
let city: String? = user?.address?.city
Transpiles to:
let name: Option<String> = Some("hello".to_string());
let len: Option<usize> = name.as_ref().map(|v| v.len());
let empty: Option<String> = None;
let len: Option<usize> = empty.as_ref().map(|v| v.len());
Smart Casting
After a null check, the type is automatically narrowed:
let value: Int? = getValue()
if value != null {
// value is now Int (not Int?)
let doubled: Int = value * 2
println!("Doubled: $doubled")
}
// Also works with pattern matching
if value is Some(v) {
println!("Got: $v")
}
Transpiles to:
let value: Option<isize> = get_value();
if let Some(value) = value {
let doubled: isize = value * 2;
println!("Doubled: {}", doubled);
}
Optional Binding
If-Let
if let name = optionalName {
println!("Hello, $name")
} else {
println!("No name provided")
}
Guard-Let
For early returns:
fn process(value: Int?) {
guard let v = value else {
println!("No value")
return
}
// v is non-optional here
println!("Processing $v")
}
Pattern Matching with Null
Use null directly in match patterns:
fn describe(opt: Int?): String {
match opt {
null -> "No value"
Some(0) -> "Zero"
Some(n) if n > 0 -> "Positive: $n"
Some(n) -> "Negative: $n"
}
}
Transpiles to:
fn describe(opt: Option<isize>) -> String {
match opt {
None => "No value".to_string(),
Some(0) => "Zero".to_string(),
Some(n) if n > 0 => format!("Positive: {}", n),
Some(n) => format!("Negative: {}", n),
}
}
Option Extension Methods
The standard library provides idiomatic methods:
let opt: Int? = 42
// Null checks (idiomatic)
opt != null // true
opt == null // false
// Transformations
opt.unwrapOr(0) // 42
opt.mapped { it * 2 } // Some(84)
opt.flatMapped { getNext(it) }
opt.filtered { it > 10 } // Some(42)
// Unwrap with closure default
opt.unwrapOrElse { computeDefault() }
See Standard Library - Option for all methods.
Combining Operators
// Safe call + null coalesce
let length = name?.len() ?? 0
// Safe call chain + null coalesce
let city = user?.address?.city ?? "Unknown"
// Safe call + method chain
let upper = name?.uppercased()?.trimmed()
// Conditional with safe call
if user?.isAdmin() ?? false {
showAdminPanel()
}
Rust Interoperability
T? and Option<T> are fully compatible:
// Call Rust function returning Option
let result: Int? = rustFunction()
// Pass Oxide nullable to Rust
fn rustExpectsOption(opt: Option<Int>) { ... }
let value: Int? = 42
rustExpectsOption(value) // Works seamlessly
Best Practices
- Prefer
??over!!- Provide meaningful defaults - Use
?.for chains - Avoid nested if-lets - Use guard-let for early returns - Cleaner than nested ifs
- Use smart casting - Let the compiler narrow types
// Good: Safe with default
let name = user?.name ?? "Guest"
// Good: Early return pattern
fn processUser(user: User?) {
guard let u = user else { return }
// Work with non-optional u
}
// Avoid: Unnecessary force unwrap
// let name = user!!.name // Might panic
See Also
- Control Flow - if-let and guard statements
- Standard Library - Option - Option extension methods
- Error Handling - Result type handling