Option: la ausencia de valor sin null
El tipo que mata al null: Option con las variantes Some y None, por qué el error del billón de dólares no puede escribirse en Rust, y cómo el compilador convierte la ausencia en una decisión obligatoria en vez de una bomba silenciosa.
Casi todos los lenguajes tienen un valor que significa “nada” y que cabe en cualquier hueco: null, nil, None, el puntero cero. Es tan cómodo como letal, porque miente sobre el tipo: una variable que dice contener un Usuario puede en realidad no contener nada, y nadie te obliga a comprobarlo. Rust toma la decisión más radical de su diseño de tipos: el null no existe. La ausencia legítima se modela con un enum normal y corriente —Option<T>— y el compilador no te deja mirar dentro sin haber contemplado antes el vacío.
- Entender por qué el
nulles un fallo del sistema de tipos, no una característica. - Leer
Option<T>como elenumde dos variantes que ya conoces:SomeyNone. - Ver cómo el compilador te obliga a manejar la ausencia antes de usar el valor.
- Distinguir “no hay valor” (
Option) de “algo falló” (Result), que llega en 7.3.
El error del billón de dólares
En 1965 Tony Hoare añadió la referencia nula a ALGOL W “simplemente porque era fácil de implementar”. Cuarenta años después la bautizó, sin ironía, como my billion-dollar mistake: el coste acumulado de todas las caídas y agujeros de seguridad que esa idea sembró. El problema no es el valor nulo en sí, sino que en la mayoría de lenguajes todo tipo lo admite en silencio. Un String de Java es en realidad “un String o una bomba”; el *p de C reza por que p no sea NULL. El tipo promete algo que el lenguaje no garantiza, y la mentira se cobra en ejecución, lejos del sitio donde se coló el vacío.
Rust deshace el nudo: no hay palabra null, ni referencia nula, ni valor vacío implícito. Una &T apunta siempre a un T válido; un String contiene siempre una cadena. Si algo puede faltar, ese “puede faltar” tiene que aparecer, explícito, en el tipo.
Option: la ausencia como un enum más
Aquí no hay magia. Option<T> es exactamente el tipo suma que construiste en 6.1, con dos variantes y genérico sobre T:
enum Option<T> {
None,
Some(T),
}
Some(valor) envuelve un valor presente; None es la ausencia. Ambas están en el preludio, así que las usas sin importar nada. Una función que quizá no tenga respuesta lo declara en su tipo de retorno:
fn primer_par(v: &[i32]) -> Option<i32> {
for &x in v {
if x % 2 == 0 {
return Some(x); // encontrado
}
}
None // recorrimos todo sin éxito
}
let a = primer_par(&[1, 3, 4, 7]); // Some(4)
let b = primer_par(&[1, 3, 5]); // None
Y leerlo es aplicar lo de 6.2 sin ninguna regla nueva: un match que contempla, obligatoriamente, las dos variantes.
match primer_par(&[1, 3, 4, 7]) {
Some(par) => println!("el primer par es {par}"),
None => println!("no había ningún par"),
}
La ausencia dejó de ser un fantasma escondido dentro de i32 y pasó a ser una variante visible en la firma. Media biblioteca estándar habla este idioma: slice::first, HashMap::get, str::parse, Iterator::next devuelven todos Option, porque todos describen operaciones que legítimamente pueden no dar nada.
Kotlin, Swift o C# moderno retrofitaron esta idea con tipos anulables (el String? de Kotlin): distinguen en el tipo lo que puede ser nulo de lo que no. Es un avance enorme sobre el null universal, y confirma que Rust acertó. Pero arrastran dos herencias que Rust no tiene: conviven con un null preexistente por compatibilidad, y ofrecen una válvula de escape de un solo carácter —el !! de Kotlin, el ! de Swift— que vuelve a hacer panic con casi nada de fricción. Rust no parte de un null que arrastrar ni da azúcar de una letra para ignorar el vacío: abrir un Option es siempre un acto visible (7.2). Diseñar desde la seguridad, en vez de añadirla después, se nota justo en los bordes.
El compilador no te deja leer el vacío
Esta es la diferencia que lo cambia todo. Option<i32> no es i32; son tipos distintos, y no puedes tratar uno como el otro:
let n: Option<i32> = primer_par(&[1, 3, 4]);
// let doble = n * 2;
error[E0369]: cannot multiply `Option<i32>` by `{integer}`
Para llegar al i32 de dentro estás obligado a abrir el Option con las herramientas de 6.2 —match, if let, let ... else— o con los métodos que verás en 7.2, y en todos los casos tienes que decir explícitamente qué ocurre cuando es None. El caso vacío es imposible de olvidar porque está grabado en el tipo: si no lo contemplas, el programa no compila. Compáralo con el null, donde olvidar la comprobación es lo normal y el castigo llega meses después, en producción.
Y como viste en 6.1, esta seguridad no cuesta memoria: gracias a la optimización de nicho, Option<&T> ocupa lo mismo que un puntero desnudo, porque el patrón nulo —que una & jamás toma— se reserva para None. Obtienes la representación de C con la garantía que C nunca dio.
A veces solo quieres saber si hay valor, sin sacarlo. Para eso están opt.is_some() y opt.is_none(), que devuelven un bool, y la macro matches!(opt, Some(x) if x > 0), que comprueba forma y condición en una sola expresión. Son atajos de lectura: para usar el valor sigues teniendo que abrir el Option (7.2), pero para un assert! o una guarda rápida son lo idiomático.
flowchart TD A[Funcion que quiza no tenga valor] --> B[Tipo de retorno Option de T] B --> S[Some con el valor dentro] B --> N[None: ausencia explicita y tipada] S --> E[El compilador exige abrir el Option] N --> E E --> F[Imposible usar el vacio por accidente] style B fill:#cba6f7,color:#11111b style S fill:#a6e3a1,color:#11111b style N fill:#f9e2af,color:#11111b style F fill:#89b4fa,color:#11111b
None no es un error
Un matiz que separa a los principiantes de quien piensa en Rust: Option responde a la pregunta “¿hay un valor?”, no a “¿algo falló?”. Que first() sobre un slice vacío dé None, o que buscar una clave ausente en un HashMap dé None, no es un error: es una respuesta perfectamente normal y esperada. El vacío aquí no lleva explicación porque no hace falta ninguna.
use std::collections::HashMap;
let edades = HashMap::from([("ana", 30)]);
let ana: Option<&i32> = edades.get("ana"); // Some(&30)
let juan: Option<&i32> = edades.get("juan"); // None: ausencia normal, no un fallo
Cuando la ausencia sí necesita justificarse —por qué no se pudo abrir el fichero, qué campo del formulario estaba mal— None se queda corto, porque no puede transportar el motivo. Ese es el territorio de Result<T, E>, que llega en 7.3: el mismo patrón de “un valor envuelto en un enum”, pero con un canal para el error. Guarda esta frontera: Option es ausencia, Result es fallo con causa.
Interioriza la asimetría profunda. El null no es un valor de tipo T: es un impostor que el lenguaje permite colar en cualquier hueco de tipo T sin avisar. Cada tipo pasa a significar en secreto “T o una bomba silenciosa”, y el compilador —que debería ser tu aliado— no sabe dónde están las bombas, así que no puede protegerte. Option<T> hace lo contrario: separa limpiamente “hay un T” de “no hay nada”, y sube esa distinción al nivel de tipos, donde el verificador sí puede razonar. La consecuencia no es solo estética; es un desplazamiento temporal del error. Lo que en un lenguaje con null es un crash a las tres de la madrugada en un caso raro, en Rust es una línea roja en tu editor mientras escribes. No has añadido comprobaciones que antes no existían —la biblioteca estándar ya devolvía la ausencia—; has hecho que ignorarlas sea imposible. Y como la optimización de nicho borra el coste, esta garantía es literalmente gratis. Ese es el trato que define a Rust: máxima seguridad, coste cero. El día que dejas de ver Option como una molestia y empiezas a verlo como el compilador negándose a dejarte mentir sobre tus datos, has cruzado la primera frontera mental del lenguaje.
- Escribe
fn buscar(v: &[i32], objetivo: i32) -> Option<usize>que devuelva la posición del elemento oNone. - Llámala con un valor presente y con uno ausente; comprueba que el tipo de retorno te obliga a pensar en ambos.
- Intenta usar el resultado como si fuera un
usizedirectamente y lee el error del compilador con calma. - Verifica con
std::mem::size_ofqueOption<&i32>mide lo mismo que&i32, peroOption<i64>no mide lo mismo quei64; razona por qué a partir de lo que aprendiste del nicho en 6.1. - Busca en la documentación tres métodos de la biblioteca estándar que devuelvan
Optiony anota qué representa elNoneen cada uno. ¿Alguno debería ser en realidad unResult?