wandres.dev
CONTROL DE FLUJO · if, loop, while, match

if let y while let

El azúcar sintáctico para hacer match de un solo patrón: if let, if let/else, let-else y while let. Su uso idiomático con Option, y las cadenas de if let de la edición 2024.

⏱ 13 min

match es exhaustivo por diseño, pero a menudo solo te interesa un patrón y quieres ignorar el resto. Escribir un match con un brazo útil y un _ => () de relleno es ruidoso. Para eso existen if let y while let: azúcar que hace matching de un único patrón sin la ceremonia. Son la herramienta cotidiana para trabajar con Option, y en la edición 2024 se encadenan para expresar condiciones que antes exigían anidar bloques.

🎯 Al terminar esta lección sabrás
  • Reconocer if let como un match de un solo brazo.
  • Usar if let/else y let-else para el camino feliz y el de fallo.
  • Consumir con while let: pilas, colas e iteradores.
  • Encadenar condiciones con las let chains de la edición 2024.

if let: un match de un solo patrón

Estas dos formas son equivalentes, pero la segunda dice lo mismo con la mitad de ruido:

let config: Option<i32> = Some(7);

// con match: un brazo útil y uno de relleno
match config {
    Some(valor) => println!("configurado a {valor}"),
    None => {}
}

// con if let: idéntico, sin el brazo vacío
if let Some(valor) = config {
    println!("configurado a {valor}");
}

La clave está en que Some(valor) = config es un patrón refutable: puede no encajar (si config fuese None). Un let normal solo admite patrones irrefutables —los que siempre encajan—; if let es precisamente lo que te deja usar un patrón refutable, ejecutando el cuerpo solo cuando encaja. Y admite un else para el caso contrario:

if let Some(valor) = config {
    println!("hay {valor}");
} else {
    println!("no había nada");
}

Y cuando solo quieres un bool —“¿encaja este patrón?”— sin ejecutar nada en el cuerpo, la macro matches! lo condensa en una sola expresión, guarda incluida:

let es_par = matches!(config, Some(n) if n % 2 == 0);

let-else: ligar o divergir

A veces quieres el valor de un Option para el resto de la función, y si no está, salir. Anidar todo el cuerpo dentro de un if let empuja la lógica hacia la derecha. La forma let-else invierte la estructura: liga en el scope actual, y si el patrón no encaja, ejecuta un bloque que debe divergir (retornar, romper un bucle o entrar en pánico):

fn procesar(entrada: Option<i32>) -> i32 {
    let Some(valor) = entrada else {
        return -1; // obligado a salir: el else no puede "seguir"
    };
    // aquí `valor` vive en el scope normal, ya desenvuelto
    valor * 2
}

El compilador exige que el else no caiga hacia abajo (su tipo debe ser el tipo never, del que hablaremos en la próxima lección). Así se garantiza que, pasado el let-else, el nombre está ligado sin ninguna duda. Es el patrón idiomático para la validación temprana: la “cláusula de guarda” de Rust.

let Ok(puerto) = "8080".parse::<u16>() else {
    eprintln!("puerto inválido");
    return;
};
// a partir de aquí `puerto` es un u16 válido, sin envolver

while let: repetir mientras encaje

while let es a while lo que if let a if: repite el cuerpo mientras el patrón siga encajando, y para en cuanto falla. Su uso estelar es vaciar una estructura cuyo “sacar” devuelve Option:

let mut pila = vec![1, 2, 3, 4, 5];

while let Some(cima) = pila.pop() { // pop() devuelve Option
    println!("saco {cima}");        // 5, 4, 3, 2, 1
}
// el bucle para solo cuando pop() devuelve None (pila vacía)

El mismo esqueleto vale para drenar un iterador a mano o recibir de un canal hasta que se cierra: en todos, None es la señal de “se acabó”.

let mut it = [1, 2, 3].into_iter();
while let Some(x) = it.next() {
    println!("{x}");
}

Esto expresa “procesa hasta agotar” con exactitud y sin un break manual: el None que marca el final es la condición de salida. Funciona igual con el recv() de un canal, con next() de un iterador que manejas a mano, o con cualquier API que señale “no hay más” con None.

ℹ️
if let renuncia a la exhaustividad a cambio de brevedad

Es un intercambio consciente, no gratis. match te obliga a tratar todos los casos; if let maneja uno y descarta el resto (con o sin else). Esa brevedad es ideal cuando de verdad solo te importa un patrón, pero es una trampa si el “resto” merecía atención. Regla práctica: si mañana añadir una variante al tipo debería obligarte a reconsiderar este código, usa match y deja que el compilador te avise; si el otro caso es un “no hacer nada” legítimo y estable, if let comunica mejor la intención.

Cadenas de if let: la edición 2024

Históricamente, comprobar dos Option a la vez obligaba a anidar:

// la "pirámide de la perdición" de antes
if let Some(usuario) = sesion {
    if let Some(rol) = usuario.rol {
        if rol.es_admin {
            conceder_acceso();
        }
    }
}

La edición 2024 estabiliza las let chains: puedes unir varios let y condiciones booleanas con && en un solo if, ligando nombres que se ven en el resto de la cadena y en el cuerpo:

// Rust 2024: una sola condición, plana y legible
if let Some(usuario) = sesion
    && let Some(rol) = usuario.rol
    && rol.es_admin
{
    conceder_acceso();
}

Cada let de la cadena solo continúa si su patrón encaja, y las variables ligadas van estando disponibles hacia la derecha. La pirámide se aplana en una lista de condiciones que se lee de arriba abajo. Es una de las mejoras de ergonomía más celebradas de la edición 2024, y requiere justamente esa edición para funcionar.

El azúcar sintáctico correcto revela la intención, no la esconde

Es tentador despreciar if let, let-else y while let como meras abreviaturas de match, cosmética sin sustancia. Sería un error de juicio. Todas se desazucaran a un matchif let Some(x) = e { c } no es más que match e { Some(x) => { c }, _ => {} }— pero eso es precisamente lo valioso: cada forma codifica una intención distinta sobre el mismo mecanismo. match dice “voy a razonar sobre todos los casos”. if let dice “solo me importa este”. let-else dice “este valor tiene que estar, o me largo”. while let dice “seguiré mientras haya”. El buen código no es el que usa la construcción más potente en todas partes, sino el que hace coincidir la construcción con la forma del problema, de modo que quien lo lea capte la intención sin descifrarla. Por eso Rust cultiva un vocabulario de azúcares alrededor del pattern matching en lugar de obligarte a escribir match siempre: no es debilidad del match, es riqueza expresiva. Y la evolución no se detiene: las let chains de la edición 2024 nacieron de observar qué escribía la gente una y otra vez —comprobaciones anidadas— y darles una forma plana. Aprender Rust no es solo aprender qué construcción es válida, sino cuál comunica mejor lo que quieres decir. Esa sensibilidad separa el código que funciona del código que además se entiende.

⚔️ Elige el azúcar correcto
  1. Reescribe un match sobre un Option<i32> que solo actúa en el caso Some usando if let, y luego añádele una rama else.
  2. Escribe una función que reciba Option<&str> y, con let-else, devuelva pronto si es None, usando el valor desenvuelto en el resto del cuerpo.
  3. Vacía un Vec con while let Some(x) = v.pop() e imprime el orden en que salen los elementos; explica por qué es el inverso de la inserción.
  4. Convierte dos if let anidados sobre dos Option distintos en una única let chain de la edición 2024 con &&.