Swift y Objective-C

[Swift básico #4] guard en Swift: acaba con la pirámide del caos

Swift guard procesa primero las condiciones de fallo y mantiene plano el flujo normal. Comprueba sus garantías frente a if, el alcance del enlace de opcionales y cuándo elegir return, throw o continue mediante ejemplos ejecutables.

6 min de lectura
Imagen de portada de [Swift básico #4] guard en Swift: acaba con la pirámide del caos

Una observación frecuente en las revisiones de código es: «Cambiemos esto a guard». El código funciona igual con if, así que ¿por qué cambiarlo? Parece una cuestión de estilo, pero guard es una construcción que Swift creó para impulsar un estilo concreto: salida temprana y ruta feliz alineada a la izquierda.

Este es el episodio 4 de la serie Swift básico. En el artículo sobre opcionales presenté brevemente guard let como herramienta de desempaquetado. Ahora analizamos guard: en qué se diferencia de if, qué garantiza el compilador y cuándo conviene if en lugar de guard.

Si quieres repasar el estado de ausencia de valor, lee primero Qué son realmente los opcionales de Swift y cómo desempaquetarlos.

El problema — la pirámide del caos

Veamos primero código de antes de guard. Una función de registro tiene mucho que comprobar: que la entrada no sea nil, que el formato sea válido y que se hayan aceptado los términos.

func signUp(email: String?, password: String?, agreed: Bool) {
    if let email = email {
        if isValidEmail(email) {
            if let password = password {
                if password.count >= 8 {
                    if agreed {
                        // Lo que realmente queríamos hacer
                        createAccount(email, password)
                    } else {
                        showError("Es necesario aceptar los términos")
                    }
                } else {
                    showError("La contraseña es demasiado corta")
                }
            } else {
                showError("Introduce una contraseña")
            }
        } else {
            showError("El formato del correo no es válido")
        }
    } else {
        showError("Introduce un correo electrónico")
    }
}

La indentación llega a cinco niveles. Esta forma se llama pirámide del caos y no solo resulta fea: también aumenta el coste de lectura. El trabajo principal de la función, createAccount, queda enterrado en lo más profundo, mientras que el tratamiento de fallo, else, está visualmente lejos de cada condición. Para responder «¿Qué ocurre si la contraseña es demasiado corta?», hay que emparejar llaves mentalmente mientras se hace scroll.

La respuesta de guard — primero los fallos, luego el trabajo principal en terreno llano

La misma función, reescrita con guard, queda así.

func signUp(email: String?, password: String?, agreed: Bool) {
    guard let email, isValidEmail(email) else {
        return showError("Comprueba el correo electrónico")
    }
    guard let password, password.count >= 8 else {
        return showError("Comprueba la contraseña (8 caracteres o más)")
    }
    guard agreed else {
        return showError("Es necesario aceptar los términos")
    }

    createAccount(email, password)
}

La estructura se ha invertido. Cada condición queda unida a su tratamiento de fallo, mientras que el trabajo principal, tras superar todos los controles, descansa sin indentación al final de la función. La mirada solo debe recorrer el código de arriba abajo una vez. La estructura lógica, «precondiciones y después trabajo principal», coincide ahora con la estructura visual.

Esta ventaja suele llamarse alineación a la izquierda de la ruta feliz. El flujo normal permanece en el nivel de indentación cero y solo las situaciones excepcionales entran en bloques. Aunque la función crezca, se mantiene la coherencia: siguiendo el extremo izquierdo se ve el escenario normal.

Tras superar los controles, el trabajo principal queda en un terreno llano sin indentación
Tras superar los controles, el trabajo principal queda en un terreno llano sin indentación

La diferencia real frente a if — dos garantías impuestas por el compilador

Es momento de preguntar: «¿Acaso if no permite también una salida temprana?». Sí: basta escribir if email == nil { return }. Pero guard es más que un if invertido: ofrece dos garantías que el compilador impone.

Primero, el bloque else debe salir del ámbito. Dentro de else de guard hay que salir del ámbito actual mediante return, throw, continue, break o fatalError; de lo contrario, la compilación falla. Con if es posible comprobar una condición y olvidar el return, pero guard detecta el error en tiempo de compilación. «Si se ha llegado aquí, la condición es verdadera» deja de ser una convención y se convierte en una garantía.

Segundo, el valor desempaquetado permanece disponible en todo el código posterior a guard. El enlace de if let solo es válido dentro de su bloque, mientras que el de guard let puede usarse en todo el ámbito restante. El valor que superó el control acompaña a la función hasta el final, sin separar artificialmente desempaquetado y uso.

Juntas, estas garantías hacen que guard también documente. El conjunto de guard al principio de una función declara su lista de precondiciones. Los contratos que no caben en la firma pueden leerse en las primeras líneas del cuerpo.

Cuándo corresponde if y no guard

¿Debemos cambiar todos los if por guard? No. El criterio es claro: guard expresa una precondición, «sin esto no se puede continuar»; if expresa una bifurcación, «en este caso haz esto y en el otro, aquello».

// ifEl lugar adecuado para if — ambas rutas son flujo normal
if user.isPremium {
    showPremiumBadge()
} else {
    showUpgradeButton()
}

No ser premium no es un fallo. Si ambas ramas continúan como escenarios normales, corresponde if. Escribirlo con guard enviaría la señal equivocada de que no ser premium es anormal.

En cambio, si fallar una comprobación termina la función, guard es adecuado aunque solo haya una comprobación. La elección sintáctica comunica significado. Quien lee espera que guard indique una precondición y que if indique una bifurcación; respetar esa expectativa es la base de la legibilidad.

También conviene señalar un antipatrón: llenar de lógica el bloque else de guard. Si allí empiezan los intentos de recuperación, los cambios de estado o los procesos largos, se rompe la promesa de que el fallo termina rápido. Si else supera tres líneas, tómalo como señal para revisar el diseño. Un tratamiento de fallo tan complejo es una bifurcación independiente o algo que debe lanzarse al llamador.

Si es una precondición, usa guard; si ambas rutas son normales, usa if
Si es una precondición, usa guard; si ambas rutas son normales, usa if

guard en bucles y código asíncrono

guard también se usa fuera de las funciones. Dentro de un bucle, se combina con continue para expresar «omitir este elemento».

for item in items {
    guard item.isValid else { continue }
    guard let url = item.downloadURL else { continue }
    process(url)
}

Cuando hay varias condiciones de filtrado, guard mantiene plano el cuerpo de for. Para condiciones simples, for item in items where item.isValid o compactMap pueden ser más concisos. Si basta una cláusula, usa where; si hay desempaquetado y varias etapas, guard resulta más cómodo.

En código asíncrono, combinar [weak self], habitual en cierres, es prácticamente un modismo.

fetchData { [weak self] data in
    guard let self else { return }
    self.update(with: data)
}

La precondición «si self ya se ha liberado, no hagas nada» se procesa en la primera línea, y el resto del código avanza en un camino llano donde self está disponible. Es el punto donde la filosofía de salida temprana de guard se encuentra con la gestión de memoria.

Resultado comprobado mediante ejecución directa

En Apple Swift 6.3.3 creé una función que recibe String?, rechaza nil en el else de guard y usa la cadena enlazada en la línea siguiente cuando hay un valor.

guard=rejected,accepted:devpaw

Este resultado coincide con mi criterio para recomendar guard en una revisión. Si una entrada fallida termina la ruta actual y solo los valores validados continúan en el cuerpo principal, guard es adecuado. Si tanto verdadero como falso son flujos de negocio normales, mantengo if en lugar de cambiarlo solo para que parezca más plano.

Resumen

  • guard admite la salida temprana a nivel del lenguaje y transforma la pirámide del caos en una lista de precondiciones seguida de un cuerpo principal plano.
  • La diferencia frente a if es la garantía del compilador: else debe salir del ámbito y los enlaces siguen siendo válidos en todo el ámbito posterior.
  • Criterio: usa guard para precondiciones cuyo fallo impide continuar e if para bifurcaciones donde ambas rutas son normales. La sintaxis comunica significado.
  • Un bloque else largo indica que guard se está usando mal. Si el tratamiento del fallo es complejo, resuélvelo con una bifurcación o lanzando un error.

En la última frase apareció «lanzar un error». De eso trata el próximo artículo: throws, las tres variantes de do-catch, try? y try!, además de Result, para completar el mapa del manejo de errores en Swift.

Lecturas recomendadas

Fuentes y verificación