guard: la salida temprana
Por qué guard produce código más legible que anidar condicionales: el contrato de salida obligatoria, el desempaquetado que sobrevive al bloque y el camino feliz sin sangrado.
guard es la construcción que mejor resume la filosofía de Swift: en vez de comprobar si todo va bien y meter el trabajo dentro del if, declaras por adelantado lo que exiges y te marchas si no se cumple. El resultado es un cuerpo de función donde las precondiciones viven arriba, en fila, y el trabajo real queda al nivel de sangrado cero. No es azúcar sobre if: el compilador impone un contrato que if no puede imponer.
- Conocer el contrato de
guardy por qué suelsedebe abandonar el ámbito. - Comparar el anidamiento creciente del
ifcon la lista plana de precondiciones. - Entender por qué lo que
guard letdesempaqueta sigue vivo después del bloque. - Saber cuándo
guarddeja de aportar yifes la herramienta correcta.
El contrato: si no se cumple, te vas
guard evalúa una condición y, si es falsa, ejecuta su else. Hasta ahí es un if invertido. La diferencia está en la regla que el compilador impone sobre ese else: está obligado a salir del ámbito actual. Con return, throw, break, continue o una llamada que nunca retorne, como fatalError. Si el bloque termina cayendo hacia abajo, no compila.
func registrar(nombre: String, edad: Int) throws {
guard !nombre.isEmpty else {
throw ValidacionError.nombreVacio
}
guard edad >= 18 else {
throw ValidacionError.menorDeEdad
}
// A partir de aqui el nombre no esta vacio y la edad es valida.
// Y esto no es una suposicion: es una garantia estructural.
almacen.insertar(Usuario(nombre: nombre, edad: edad))
}
Esa obligación es justo lo que da valor a la construcción. Con un if que comprueba lo contrario, nada impide que alguien olvide el return y el flujo continúe con datos inválidos; con guard esa negligencia es un error de compilación. La condición deja de ser una recomendación y pasa a ser una precondición verificada.
No existe un guard sin else, y su bloque no puede quedar vacío. El lenguaje te obliga a responder a la pregunta incómoda: ¿qué hago cuando esto no se cumple? Muchos errores nacen de no haberla contestado nunca. Aquí no se puede posponer.
De la pirámide a la banda plana
Compara las dos formas de escribir la misma lógica. Primero, con condicionales anidados, cada requisito empuja el trabajo un nivel más a la derecha:
func procesar(_ datos: Data?) -> String? {
if let datos = datos {
if let texto = String(data: datos, encoding: .utf8) {
if !texto.isEmpty {
return texto.uppercased()
} else {
return nil
}
} else {
return nil
}
} else {
return nil
}
}
Con guard los requisitos se alinean arriba y el resultado queda solo, sin sangrado y sin ruido:
func procesar(_ datos: Data?) -> String? {
guard let datos else { return nil }
guard let texto = String(data: datos, encoding: .utf8) else { return nil }
guard !texto.isEmpty else { return nil }
return texto.uppercased()
}
Lo que ha cambiado no es solo la cantidad de líneas. En la primera versión, entender qué devuelve la función obliga a rastrear con qué if empareja cada else, un trabajo que crece con el cuadrado de la profundidad. En la segunda, el lector aplica una lectura secuencial: cada línea añade una garantía y ninguna la quita. El camino feliz es la última línea, siempre al mismo nivel, y los caminos de fallo están agrupados donde ocurre cada uno.
flowchart TD
A[Entrada a la funcion] --> B{Precondicion 1}
B -->|Falla| S1[Salir con error o nil]
B -->|Cumple| C{Precondicion 2}
C -->|Falla| S2[Salir con error o nil]
C -->|Cumple| D{Precondicion 3}
D -->|Falla| S3[Salir con error o nil]
D -->|Cumple| E[Camino feliz sin sangrado]El desempaquetado que sobrevive
Aquí está la ventaja técnica que ninguna reordenación de if puede reproducir. Cuando if let desempaqueta un opcional, el nombre nuevo solo existe dentro de las llaves del if. Cuando lo hace guard let, el nombre existe desde esa línea hasta el final del ámbito que lo contiene.
func saludar(a usuario: Usuario?) -> String {
guard let usuario else { return "Hola, invitado" }
// usuario ya no es opcional en TODO el resto de la funcion
let apodo = usuario.apodo ?? usuario.nombre
let saludo = "Hola, \(apodo)"
return saludo
}
Es una consecuencia lógica del contrato: si el else está obligado a abandonar el ámbito, entonces todo el código que sigue solo puede alcanzarse cuando la condición fue cierta, y el compilador puede afinar el tipo a partir de ahí. La forma abreviada guard let usuario sin el = usuario de la derecha, disponible desde Swift 5.7, evita además el ritual de repetir el mismo nombre tres veces.
if let
Ata el valor al bloque. Ideal cuando el desempaquetado solo interesa para una rama breve y el resto de la función sigue igual.
guard let
Ata el valor al resto del ámbito. Ideal cuando sin ese valor la función no tiene nada que hacer.
Un solo guard admite además varias condiciones separadas por comas, mezclando desempaquetados y comprobaciones booleanas; cada una puede usar lo que ataron las anteriores. Y con guard case se aplica cualquier patrón, igual que en un switch:
func publicar(_ borrador: Borrador?) throws {
guard let borrador,
let titulo = borrador.titulo,
!titulo.isEmpty,
borrador.palabras >= 300
else { throw PublicacionError.incompleto }
guard case .revisado(let revisor) = borrador.estado else {
throw PublicacionError.sinRevisar
}
almacen.publicar(borrador, titulo: titulo, revisadoPor: revisor)
}
Cuatro requisitos y un patrón, una sola salida, cero sangrado añadido. La versión con condicionales anidados de esta misma función tendría cinco niveles de profundidad y cinco else que emparejar a ojo.
Cuándo guard estorba
guard no es siempre la respuesta. Su lógica es “si esto falla, aquí no hay nada más que hacer”, y forzarlo donde ambas ramas son igual de legítimas produce código peor. Si tienes dos caminos simétricos, ambos con trabajo real, un if con else dice la verdad y un guard la disfraza de excepción. Si necesitas diez guard seguidos, el problema no es el flujo sino el diseño: probablemente esa función hace demasiado, o el tipo de entrada admite estados que no deberían ser representables.
Un guard cuyo else contiene quince líneas de lógica de recuperación ha invertido la jerarquía: lo excepcional ocupa el espacio y lo normal queda escondido. Cuando el bloque de salida crece más que el camino feliz, vuelve a if o extrae la recuperación a su propia función.
La virtud de guard no es estética, es epistemológica: reordena dónde vive el conocimiento dentro de una función. Con condicionales anidados, lo que sabes en un punto del código depende de la ruta que te llevó hasta él, y esa ruta es invisible en la línea que estás leyendo; hay que reconstruirla mentalmente subiendo por las llaves, y el esfuerzo crece con la profundidad. Con guard, el conocimiento es acumulativo y local: cualquier línea sabe, sin mirar arriba, que todas las precondiciones anteriores se cumplen, porque el compilador ha demostrado que el flujo no puede llegar hasta ella de otro modo. Es la misma idea que sostiene la programación por contrato de Meyer o las precondiciones de Dijkstra, solo que verificada por el sistema de tipos en vez de por un comentario. Y el efecto secundario es el más valioso: cada guard que escribes es una hipótesis explícita sobre tus datos, así que la lista de guard al inicio de una función se convierte en su especificación legible. Si esa lista se vuelve larga, no has escrito mala lógica de control: has descubierto que tu tipo de entrada permite demasiados estados inválidos, y la solución correcta no es más guard, sino un tipo mejor que haga imposibles esos estados y deje esas comprobaciones sin sentido.
- Escribe una función que reciba tres opcionales y devuelva un resultado combinado, primero con
if letanidados y luego conguard let. Cuenta niveles de sangrado en cada versión. - Quita el
returndel bloqueelsede unguardy lee el error exacto del compilador. - Comprueba que una variable desempaquetada con
if letno existe fuera del bloque, y que conguard letsí. - Convierte un
guardcuyoelsetenga diez líneas en unifconelse, y decide cuál de las dos versiones prefieres y por qué. - Usa
guarddentro de un bucleforsaliendo concontinue, y explica por qué ahíreturnno sería lo correcto.