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.
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.
- Distinguir
if letdeguard letpor 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 caseyfor caseusando 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.
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.
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:#11111bAquí 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.
- Toma una función tuya con dos
if letanidados y reescríbela con un únicoguard letde varios ligados; compara la indentación del camino feliz. - Escribe una función que reciba un
Int?y use unswitchcon0?,let n? where n < 0,let n?ynil, y comprueba qué dice el compilador si borras una rama. - Recorre un
[String?]confor case let x?y luego concompactMap; razona en qué situación prefieres cada uno. - Convierte un
if let self = selfde un closure aguard let self else { return }y explica por qué el segundo evita indentar el resto del cuerpo. - Escribe deliberadamente un
guardcuyoelseno salga del ámbito y lee el error del compilador: es la garantía que hace fiable aguard.