Desenvolver Option con seguridad: de match a los métodos
Cómo sacar el valor de un Option sin traicionar la garantía: match e if let para el control total, y la familia unwrap, expect, unwrap_or y unwrap_or_else. Por qué unwrap es un olor de código fuera de prototipos y tests.
Tener un Option<T> es tener una promesa a medias: quizá haya un T, quizá no. Desenvolverlo es cobrar esa promesa, y hay dos maneras de hacerlo: la honesta, que contempla el vacío, y la temeraria, que apuesta a que nunca ocurrirá y hace panic! si pierde. Rust te da ambas a propósito, pero la frontera entre “cómodo” y “código que revienta en producción” pasa justo por aquí. Aprender a desenvolver bien es aprender a no reintroducir el null que Option acababa de matar.
- Desenvolver con control total usando
matcheif letsobreOption. - Conocer la familia de métodos:
unwrap,expect,unwrap_or,unwrap_or_else,unwrap_or_default. - Distinguir el defecto ansioso (
unwrap_or) del perezoso (unwrap_or_else). - Entender por qué
unwrapes un olor de código salvo en prototipos, ejemplos y tests.
match e if let: el control total
Las herramientas de 6.2 se aplican tal cual a Option, porque Option no es más que un enum. match te obliga a cubrir los dos casos y desestructura el valor en la rama Some:
fn saludo(nombre: Option<&str>) -> String {
match nombre {
Some(n) => format!("Hola, {n}"),
None => "Hola, desconocido".to_string(),
}
}
Cuando solo te importa el caso con valor, if let evita el ceremonial, y let ... else extrae al hilo principal de la función obligando a que el camino vacío diverja:
if let Some(n) = nombre {
println!("hay nombre: {n}");
}
fn inicial(nombre: Option<&str>) -> char {
let Some(n) = nombre else {
return '?'; // sin nombre: salimos ya
};
n.chars().next().unwrap_or('?')
}
Esto siempre es correcto porque nunca ignora el None. El resto de la lección son atajos: azúcar sobre este match, con distintos compromisos.
La familia unwrap: atajos con precio
A veces escribir un match de dos ramas para un caso trivial es ruido. La biblioteca estándar ofrece métodos que colapsan ese match en una llamada. La diferencia entre ellos es qué hacen con el None:
unwrap()
Devuelve el valor, o hace panic! si es None. El mensaje es genérico: “called Option::unwrap() on a None value”. Rápido de teclear, opaco de depurar.
expect(msg)
Igual que unwrap, pero el mensaje del pánico lo escribes tú. Sirve para documentar la invariante que garantiza que no será None.
unwrap_or(x)
Devuelve el valor, o x si es None. Nunca hace panic. El defecto x se evalúa siempre, se use o no.
unwrap_or_else(f)
Devuelve el valor, o llama al closure f solo si es None. Perezoso: ideal cuando calcular el defecto es caro.
En código:
let x: Option<i32> = Some(5);
let vacio: Option<i32> = None;
x.unwrap(); // 5
x.expect("x debería venir del parser"); // 5, con mensaje si fallara
vacio.unwrap_or(0); // 0
vacio.unwrap_or_else(|| calculo_caro()); // llama al closure solo aquí
vacio.unwrap_or_default(); // 0, el Default de i32
Ansioso frente a perezoso
unwrap_or y unwrap_or_else parecen intercambiables, pero esconden una diferencia que importa. El argumento de unwrap_or se evalúa antes de la llamada, exista o no el valor; el de unwrap_or_else solo se evalúa si el Option es None:
// Si el Option es Some, la conexión se abre EN VANO y se descarta.
let cfg = opcion.unwrap_or(abrir_conexion_por_defecto());
// La conexión solo se abre cuando de verdad falta el valor.
let cfg = opcion.unwrap_or_else(|| abrir_conexion_por_defecto());
Regla práctica: si el defecto es un literal o algo baratísimo (0, "", un Vec::new), usa unwrap_or por legibilidad. Si construirlo tiene coste o efectos secundarios —abrir un fichero, reservar memoria, consultar la red— usa unwrap_or_else y paga solo cuando toca. Para el valor por defecto del tipo, unwrap_or_default es el atajo idiomático.
El caso más común de este error es unwrap_or(Vec::new()) o unwrap_or(String::new()): parecen inocentes, pero reservan (aunque sea de forma barata) en cada llamada aunque el valor esté presente. Con colecciones y todo lo que asigne, acostúmbrate a unwrap_or_else(Vec::new) o, mejor aún, unwrap_or_default(). Es el tipo de detalle que no se nota hasta que aparece en un bucle caliente en un profiler.
Por qué unwrap es un olor de código
unwrap y expect no manejan el None: lo convierten en un panic! que aborta el hilo. Es decir, cogen la ausencia que Option te obligaba a considerar y la transforman de nuevo en un fallo de ejecución —exactamente el billón de dólares que 7.1 acababa de desterrar—. Por eso, en código de biblioteca o de producción, cada unwrap es una deuda: una afirmación no demostrada de que “aquí nunca será None”.
Esto no significa que estén prohibidos. Son perfectamente legítimos en:
- prototipos y borradores, donde quieres avanzar y ya endurecerás luego;
- ejemplos y documentación, donde el
matchdistraería del punto; - tests, donde un
Noneinesperado debe hacer fallar el test ruidosamente; - invariantes de verdad imposibles, y ahí siempre
expectcon un mensaje que explique por qué es imposible, nuncaunwrapa secas.
flowchart TD A[Tengo un Option y necesito el valor] --> B[Debo actuar en los dos casos] B --> M[match exhaustivo] A --> C[Solo me importa el caso con valor] C --> L[if let o let else] A --> D[Quiero un valor por defecto si falta] D --> E[unwrap_or o unwrap_or_else] A --> F[Garantizo que jamas sera None] F --> G[expect con la invariante documentada] style M fill:#a6e3a1,color:#11111b style L fill:#89b4fa,color:#11111b style E fill:#f9e2af,color:#11111b style G fill:#f38ba8,color:#11111b
El sistema de tipos de Rust es, en el fondo, un demostrador: cuando compila, ha probado que ciertas cosas malas no pueden pasar. unwrap es la puerta por la que le dices “esta parte no la demuestres tú, la garantizo yo”. Y ese es justo el peligro: has salido del régimen donde el compilador te cubre y has vuelto al de C, donde la corrección depende de que un humano tenga razón. La diferencia entre un unwrap disperso y un expect("el índice viene validado tres líneas arriba") no es cosmética: el segundo convierte tu suposición en un enunciado auditable, de modo que cuando —porque en software siempre es cuando, no si— la invariante se rompa, el pánico te diga en un segundo qué creencia era falsa, en vez de escupir “None value” sin contexto. Programar bien en Rust no es evitar todo unwrap, es tratar cada uno como una deuda que anotas conscientemente y que estás dispuesto a defender en una revisión de código. La mayoría de esas deudas, además, se saldan gratis: un unwrap_or, un ? (7.5) o un match de dos líneas hacen el mismo trabajo sin apostar la estabilidad del proceso. Cuando veas un unwrap, pregúntate siempre: ¿estoy demostrando esto, o solo esperando que se cumpla?
- Escribe una función que reciba
Option<i32>y devuelva el doble, o0si no hay valor, usandounwrap_or. - Reescríbela con
matchy luego conunwrap_or_else; explica en qué casos preferirías cada una. - Crea un
Option<String>aNoney comparaunwrap_or(String::from("x"))conunwrap_or_else(|| String::from("x")): instrumenta con unprintln!dentro para ver cuándo se ejecuta cada defecto. - Sustituye un
unwrap()por unexpect("...")con un mensaje que documente la invariante real; provoca el pánico y compara la utilidad de ambos mensajes. - Busca en un proyecto tuyo (o en uno de GitHub) tres
unwrapy clasifícalos: ¿prototipo, invariante defendible, o deuda que deberías saldar con??