El inventor del null, Tony Hoare, lo llamó su "error del billón de dólares". Rust no tiene null. En su lugar, usa Option<T>: un enum que representa de forma explícita la posibilidad de que un valor esté ausente. El compilador te obliga a manejar ambos casos, eliminando toda una categoría de bugs.
La definición de Option
enum Option<T> {
None, // ausencia de valor
Some(T), // presencia de valor de tipo T
}
Está en el preludio de Rust: no necesitas importarla. Puedes escribir Some(5) o None directamente.
Crear y usar Option con match
fn dividir(a: f64, b: f64) -> Option<f64> {
if b == 0.0 {
None
} else {
Some(a / b)
}
}
fn main() {
match dividir(10.0, 2.0) {
Some(resultado) => println!("Resultado: {}", resultado),
None => println!("División por cero"),
}
match dividir(5.0, 0.0) {
Some(r) => println!("Resultado: {}", r),
None => println!("División por cero"),
}
}
unwrap_or: valor por defecto
let valor = dividir(10.0, 0.0).unwrap_or(0.0);
println!("{}", valor); // 0.0
unwrap_or(x) devuelve el valor interno si es Some, o x si es None. Hay variantes útiles:
unwrap_or_default(): usa el valor por defecto del tipo.unwrap_or_else(|| calcular()): llama a una closure si esNone.
map: transformar el valor si existe
let longitud: Option<usize> = Some(String::from("hola")).map(|s| s.len());
// Some(4)
let nada: Option<usize> = None::<String>.map(|s| s.len());
// None
map aplica una función al interior si es Some, y propaga None si no hay valor. Nunca panics.
and_then: encadenar operaciones que pueden fallar
fn buscar_usuario(id: u32) -> Option<String> {
if id == 1 { Some(String::from("Ana")) } else { None }
}
fn obtener_email(nombre: &str) -> Option<String> {
if nombre == "Ana" { Some(String::from("[email protected]")) } else { None }
}
fn main() {
let email = buscar_usuario(1)
.as_deref()
.and_then(|nombre| obtener_email(nombre));
println!("{:?}", email); // Some("[email protected]")
}
and_then es como map, pero la función devuelve un Option en lugar de un valor directo. Evita el anidamiento Option<Option<T>>.
if let para un solo caso
if let Some(valor) = dividir(10.0, 2.0) {
println!("OK: {}", valor);
}
// No hace nada si es None
Propagar None con el operador ?
fn procesar() -> Option<String> {
let usuario = buscar_usuario(1)?; // si es None, retorna None
let email = obtener_email(&usuario)?; // idem
Some(format!("Email de {}: {}", usuario, email))
}
El operador ? en contexto de Option devuelve None si el valor es None, o extrae el Some y continúa. Elimina el boilerplate de los match anidados.
Cuándo evitar unwrap
unwrap() extrae el valor de Some o hace panic si es None. En producción, úsalo solo cuando tengas la certeza lógica de que el valor existe y quieras que el programa falle ruidosamente si no:
// OK en main o en tests, con certeza lógica
let config = std::env::var("DATABASE_URL").ok().unwrap();
// Mejor en producción:
let config = std::env::var("DATABASE_URL")
.ok()
.unwrap_or_else(|| String::from("sqlite://default.db"));
Resumen
Option<T>tiene dos variantes:Some(T)yNone.- Sustituye al
null: el tipo hace explícita la posibilidad de ausencia. matcheif letpara extraer el valor con pattern matching.unwrap_or,map,and_thenpara trabajar conOptionde forma funcional.- El operador
?propagaNonesin boilerplate. - Evita
unwrap()en producción salvo que tengas certeza absoluta.
El siguiente artículo cubre match en profundidad: pattern matching exhaustivo con rangos, enums, desestructuración, guards y match como expresión.
