wandres.dev
OPTION Y RESULT · sin null ni excepciones

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.

⏱ 16 min

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.

🎯 Al terminar esta lección sabrás
  • Desenvolver con control total usando match e if let sobre Option.
  • 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é unwrap es 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.

⚠️
unwrap_or(vec![...]) es una trampa silenciosa

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 match distraería del punto;
  • tests, donde un None inesperado debe hacer fallar el test ruidosamente;
  • invariantes de verdad imposibles, y ahí siempre expect con un mensaje que explique por qué es imposible, nunca unwrap a 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
Cada unwrap es una obligación de prueba que le firmas al compilador

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?

⚔️ Desenvuelve sin apostar el proceso
  1. Escribe una función que reciba Option<i32> y devuelva el doble, o 0 si no hay valor, usando unwrap_or.
  2. Reescríbela con match y luego con unwrap_or_else; explica en qué casos preferirías cada una.
  3. Crea un Option<String> a None y compara unwrap_or(String::from("x")) con unwrap_or_else(|| String::from("x")): instrumenta con un println! dentro para ver cuándo se ejecuta cada defecto.
  4. Sustituye un unwrap() por un expect("...") con un mensaje que documente la invariante real; provoca el pánico y compara la utilidad de ambos mensajes.
  5. Busca en un proyecto tuyo (o en uno de GitHub) tres unwrap y clasifícalos: ¿prototipo, invariante defendible, o deuda que deberías saldar con ??