wandres.dev
OPCIONALES A FONDO · el arte de la ausencia

Desenvolver bien: if let, guard let y switch

Las formas seguras de abrir un opcional y qué comunica cada una. La sintaxis abreviada y su sombreado deliberado, el patrón con interrogación en switch y for, y cómo desenvolver varios valores sin construir una pirámide.

⏱ 17 min

Sabiendo que Optional es un enum, desenvolver deja de ser un ritual y pasa a ser lo que siempre fue: emparejar patrones sobre dos casos. Lo interesante no es cómo se abre la caja —hay cuatro formas y todas funcionan— sino qué le dice cada una al lector sobre la naturaleza de la ausencia. Elegir mal no rompe el programa; rompe la explicación de por qué el programa es correcto.

🎯 Al terminar esta lección sabrás
  • Distinguir if let de guard let por lo que afirman, no por su sintaxis.
  • Usar la forma abreviada y entender por qué el sombreado de nombre es deliberado.
  • Emparejar opcionales con switch, if case y for case usando el patrón con interrogación.
  • Desenvolver varios valores a la vez sin anidar bloques.

Dos preguntas distintas: if let y guard let

Ambas construcciones abren el opcional y ligan el contenido a un nombre no opcional. La diferencia no es estilística: es el alcance de lo que afirman.

// if let: la ausencia es una rama legítima del flujo
if let apodo = usuario.apodo {
    mostrar(apodo)
} else {
    mostrarNombreLegal(usuario.nombre)
}

// guard let: la ausencia significa que esta función no tiene nada que hacer
func enviar(a destinatario: Usuario) throws {
    guard let correo = destinatario.correo else {
        throw EnvioError.sinDireccion
    }
    // a partir de aquí, y hasta el final de la función, correo es un String
    try transporte.enviar(a: correo)
}

if let dice hay dos caminos y los dos son válidos. guard let dice esto es una precondición: si falta, salgo. Por eso el ligado de guard sobrevive al bloque y llega hasta el final del ámbito, mientras que el de if muere en la llave. La regla práctica es leerlos en voz alta: si el else acaba en return, throw, break, continue o fatalError, lo que estabas escribiendo era un guard.

Esa obligación de salir no es una convención: el compilador la impone. El else de un guard debe transferir el control fuera del ámbito, y si no lo hace, no compila. Es lo que convierte un guard en una afirmación verificada sobre el resto de la función.

💡
El guard aplana el camino feliz

Una función escrita con guard tiene todas sus salidas anómalas arriba y su lógica real a nivel cero de indentación. Una escrita con if let encadenados empuja la lógica a la derecha y esconde el camino principal dentro de los pliegues. Cuando revises código, cuenta la indentación del retorno útil: si está a tres niveles de profundidad, casi siempre había un guard esperando a ser escrito.

La forma abreviada y el sombreado

Desde Swift 5.7 puedes omitir el lado derecho cuando el nombre no cambia. if let apodo equivale a if let apodo = apodo.

func saludar(_ apodo: String?) {
    if let apodo {           // el nuevo apodo es String y tapa al parámetro
        print("Hola, \(apodo)")
    }
    // aquí apodo vuelve a ser String? porque el ligado ya no está vigente
}

Ese sombreado incomoda a quien viene de lenguajes donde reusar un nombre es un olor. Aquí es exactamente lo contrario: el nombre designa el mismo concepto, y dentro del bloque designa además la versión con valor garantizado. Mantener dos nombres —apodo y apodoDesenvuelto— reintroduce justo el error que el opcional quería evitar, porque nada impide usar el opcional cuando querías el otro. El sombreado hace que el nombre correcto sea el único disponible.

Tres matices que conviene tener claros. Primero, la forma abreviada exige un identificador simple: if let self.apodo no compila, y para propiedades hace falta if let apodo = self.apodo. Segundo, existe if var cuando necesitas mutar la copia desenvuelta. Tercero, en closures que capturan self de forma débil, guard let self else { return } es la forma canónica y desde Swift 5.8 funciona también con la sintaxis abreviada.

El patrón con interrogación: switch, if case y for case

Como Optional es un enum, todo el motor de patrones de Swift funciona sobre él, y ahí es donde aparece la forma más expresiva de tratar la ausencia: cuando nil no es un caso aparte sino un valor más que comparar.

switch respuesta {          // respuesta es Int?
case 0?:
    print("cero explícito")
case let n? where n < 0:
    print("negativo: \(n)")
case let n?:
    print("positivo: \(n)")
case nil:
    print("sin respuesta")
}

0? empareja con Optional.some(0); let n? es azúcar de case .some(let n); nil empareja el caso vacío. Un solo switch cubre la ausencia y la forma del contenido a la vez, y el compilador comprueba la exhaustividad por ti. El mismo patrón sirve para recorrer colecciones descartando huecos, sin filtrar antes:

let entradas: [String?] = ["ana", nil, "juan"]
for case let nombre? in entradas {
    print(nombre)           // ana, juan: los nil ni entran al cuerpo
}

if case let .some(codigo) = respuesta, codigo > 200 {
    print("código alto")
}

El mismo patrón sostiene una construcción que usas a diario sin reparar en ella. while let consume cualquier cosa que produzca opcionales y termina en el primer vacío:

var iterador = numeros.makeIterator()
while let n = iterador.next() {   // next devuelve Int?; el nil corta el bucle
    print(n)
}

Eso es, literalmente, el motor del for in: el bucle que escribes siempre es azúcar sobre un while let que agota un iterador. La ausencia no es aquí un caso raro que gestionar, sino la señal de terminación de toda la iteración del lenguaje.

ℹ️
Cuándo el switch gana al if let

Usa if let cuando solo te importa si hay valor. Pasa a switch cuando te importa qué valor es, o cuando la ausencia es un caso al mismo nivel que los demás y quieres que el compilador te obligue a nombrarla. El switch sobre un opcional es la herramienta correcta cada vez que te sorprendas escribiendo un if let cuyo cuerpo empieza inmediatamente con otro if.

Varios valores a la vez

Ni if let ni guard let se limitan a un ligado. Puedes encadenar condiciones separadas por comas, mezclando desenvolvimientos y expresiones booleanas, y cada ligado está disponible para los siguientes en la misma lista.

guard
    let usuario = sesion.usuario,
    let correo = usuario.correo,       // usa el ligado anterior
    !correo.isEmpty,                   // condición booleana en la misma lista
    let plan = catalogo[usuario.planID]
else {
    return .noDisponible
}
return .listo(usuario: usuario, correo: correo, plan: plan)

La evaluación es perezosa y de izquierda a derecha, con cortocircuito: si sesion.usuario es nil, usuario.correo ni se evalúa. Eso es lo que hace legítimo apoyar un ligado en el anterior. Y es lo que sustituye a la pirámide de if anidados que producían las versiones antiguas del lenguaje.

Esa lista, eso sí, solo distingue dos desenlaces: están todos o falta alguno. Cuando lo que importa es qué combinación de presencias tienes, el switch sobre una tupla expresa la matriz completa y el compilador comprueba que no dejas huecos:

switch (fecha, hora) {
case let (f?, h?):  programar(fecha: f, hora: h)
case let (f?, nil): programarDiaCompleto(f)
case (nil, _):      pedirFecha()
}
flowchart TD
A[Opcional a la vista] --> B{La ausencia es un camino valido}
B -- Si --> C[if let o switch]
B -- No, es una precondicion --> D[guard let con salida]
C --> E{Importa el valor concreto}
E -- Si --> F[switch con patron de interrogacion]
E -- No --> G[if let corto]
style D fill:#a6e3a1,color:#11111b
style F fill:#cba6f7,color:#11111b
style G fill:#89b4fa,color:#11111b
Desenvolver no es una molestia: es donde decides el significado de la ausencia

Aquí está el cambio de mentalidad que separa a quien pelea con los opcionales de quien los usa para pensar. El compilador no te obliga a desenvolver para fastidiarte, ni siquiera principalmente para evitar un crash: te obliga a decidir qué significa que no haya valor, en el punto exacto del programa donde tienes el contexto para decidirlo. Y esa decisión es información de diseño, no burocracia. Un guard let con throw declara que el valor era una precondición y que su ausencia es un fallo de quien llama. Un if let con else declara que la ausencia es un estado normal con su propia presentación. Un case nil dentro de un switch declara que la ausencia es un caso más del dominio, al mismo nivel que los otros. Tres construcciones, tres afirmaciones distintas sobre el mundo, y todas verificadas por el compilador. Por eso elegir la forma de desenvolver es un acto de documentación ejecutable: el lector que llegue dentro de un año no tendrá que adivinar si ese nil era esperado, tolerado o imposible, porque tú ya se lo dijiste en la única sintaxis que no puede mentir. Un opcional mal desenvuelto rara vez rompe el programa; casi siempre rompe la explicación.

⚔️ Elige la forma que dice la verdad
  1. Toma una función tuya con dos if let anidados y reescríbela con un único guard let de varios ligados; compara la indentación del camino feliz.
  2. Escribe una función que reciba un Int? y use un switch con 0?, let n? where n < 0, let n? y nil, y comprueba qué dice el compilador si borras una rama.
  3. Recorre un [String?] con for case let x? y luego con compactMap; razona en qué situación prefieres cada uno.
  4. Convierte un if let self = self de un closure a guard let self else { return } y explica por qué el segundo evita indentar el resto del cuerpo.
  5. Escribe deliberadamente un guard cuyo else no salga del ámbito y lee el error del compilador: es la garantía que hace fiable a guard.