Swift – La Sentencia Guard

Hoy aprenderemos sobre la sentencia guard. Abordaremos la Pyramid of Doom y el Early Return, analizaremos varios ejemplos donde la sentencia guard resulta bien útil.

La sentencia guard funciona de manera parecida a if ya que ejecuta un código basado en una expresión booleana. Pero a diferencia de if el código dentro del bloque guard solamente se ejecuta si la condición no se cumple, es decir cuando es false.

Luego de esta explicación quizás muchos pensarán que es algo redundante, inútil y de la cual podemos prescindir. Claramente el comportamiento de guard puede ser sustituido fácilmente por if ¿Cierto?

La razón de la existencia de guard viene dada por características intrínsecas que la hacen diferente y no por oscuros deseos de confundirte aún más en tu proceso de aprendizaje. Así que la respuesta es no, aunque puedan lucir similares a simple vista no lo son.

Pyramid of doom

Antes de continuar hablando de guard creo que debemos entender la necesidad tras su uso. Comencemos por imaginar que tenemos una pantalla donde vamos a registrar nuevos usuarios.

Hay cuatro campos donde se especificará el nombre, correo electrónico, un password y la confirmación del mismo, por último un botón con el cual enviaremos estos datos hacia nuestra base de datos:

var nameEdit: UITextField?
var emailEdit: UITextField?
var passwordEdit: UITextField?
var confirmEdit: UITextField?

func registerOnTouch(sender: UIButton) {
    
    if let name = nameEdit?.text {
        
        if let email = emailEdit?.text {
            
            if let password = passwordEdit?.text {
                
                if let confirm = confirmEdit?.text {
                    
                    if password == confirm {

                        print("Registrando nuevo usuario: \(name) con email: \(email) y password: \(password)")
                        
                    } else {
                        
                        print("Ha fallado la confirmación!")
                        
                    } // else

                } else {
                    
                    print("Confirme el password por favor!")
                    
                } // else

            } else {
                
                print("Escriba un password por favor!")
                
            } // else

        } else {
            
            print("Especifique un correo electrónico por favor!")
            
        } // else

    } else {
        
        print("Especifique un nombre de usuario por favor!")
        
    } // else

} // registerOnTouch

nameEdit = UITextField()
emailEdit = UITextField()
//passwordEdit = UITextField()
confirmEdit = UITextField()

nameEdit?.text = "josuevhn"
emailEdit?.text = "josuevhn@example.com"
passwordEdit?.text = "123"
confirmEdit?.text = "123"

registerOnTouch(sender: UIButton())

Aquí tenemos una clásica Pyramid of Doom (pirámide de la perdición), un código horrible y que debemos de evitar a toda costa. El enfoque tras este código básicamente se resume en que la constante generada con:

if let name = nameEdit?.text

solamente puede ser usada desde dentro del ámbito de las llaves de este bloque if. Así que el programador luego de comprobar que nameEdit no es nil pues anida la comprobación de emailEdit y así consecutivamente hasta llegar a la línea 18 donde necesita poder acceder a todas las constantes antes creadas en pos de poder almacenarlas, generando así la antes mencionada pirámide de la perdición.

En la línea 53 hemos comentado la inicialización de passwordlEdit en pos de comprobar nuestro horrible código, obteniendo la siguiente salida en pantalla:

Escriba un password por favor!

Al mismo tiempo si eliminamos el comentario sobre la línea 53 la salida sería la esperada:

Registrando nuevo usuario: josuevhn con email: josuevhn@example.com y password: 123

Creo que está de más decir que hay mejores opciones al código anterior, más legibles y fáciles de mantener.

Early return

Un mejor enfoque sería el de un retorno temprano, aquel donde primero comprobamos los datos con los que trabajaremos, o en este caso que los campos de texto no sean nil y que tampoco estén vacíos:

func registerOnTouch(sender: UIButton) {

    if let name = nameEdit?.text, name == "" {
        
        print("Especifique un nombre de usuario por favor!")
        
        return
        
    } // if

    if let email = emailEdit?.text, email == "" {
     
        print("Especifique un correo electrónico por favor!")
        
        return
        
    } // if
    
    if let password = passwordEdit?.text, password == "" {
    
        print("Escriba un password por favor!")
        
        return
        
    } // if

    if let confirm = confirmEdit?.text, confirm == "" {
     
        print("Confirme el password por favor!")
        
        return
        
    } // if
    
    if password != confirm {
     
        print("Ha fallado la confirmación.")
        
        return
        
    } // if

    print("Registrando nuevo usuario: \(name!) con email: \(email!) y password: \(password!)")

} // registerOnTouch

para al final añadir el nuevo registro luego de todas las validaciones.

El problema con este código es que no compila y de hecho muchas veces este último ejemplo es el que genera la pirámide de la perdición, en un esfuerzo por corregir los 5 errores que nos informa Xcode. Uno de estos:

Playground execution failed: error: TheGuardStatement.playground:38:8: error: use of unresolved identifier 'password'

La razón tras estos errores es la que ya comentamos: el tiempo de vida de las constantes creadas con «if let» está limitado al ámbito de ese bloque if.

Por ende el compilador no puede encontrar en la línea 35 ninguna referencia a las constantes password o confirm y de igual manera sucede en la línea 43 con name, email y password.

En este punto la única manera de salvar el código anterior sería desempaquetar nuevamente todas las variables opcionales (name, email, password…), siempre asegurándonos de que todas las comprobaciones ya han sido efectuadas:

func registerOnTouch(sender: UIButton) {

    ...

    if let confirm = confirmEdit?.text, confirm == "" {
     
        print("Confirme el password por favor!")
        
        return
        
    } // if
    
    let name = nameEdit?.text
    let email = emailEdit?.text
    let password = passwordEdit?.text
    let confirm = confirmEdit?.text
    
    if password != confirm {
     
        print("Ha fallado la confirmación.")
        
        return
        
    } // if

    print("Registrando nuevo usuario: \(name!) con email: \(email!) y password: \(password!)")

} // registerOnTouch

por lo que añadimos este código a continuación del último bloque «if let«. Con esto logramos que las instrucciones que vienen a continuación puedan encontrar las referencias correspondientes.

¿Por qué hemos dicho entonces que este era un mejor enfoque?

Pues porque realmente lo es, el problema lo ha generado «if let» y evidentemente hay una solución mucho más elegante y legible a una horrible pirámide de la perdición o al parche que acabamos de comentar.

Transferencia de control

El objetivo de guard es alterar el control de flujo de un programa, usualmente dentro del ámbito de un ciclo o de una función, tal y como ya hemos explicado esto ocurre siempre y cuando una o varias de las condiciones no sean válidas.

Una característica que viene como anillo al dedo para un enfoque Early Exit (salida temprana). vamos que se presta de manera natural a servir de validación para nuestras funciones.

Teniendo en cuenta la forma que adopta la sentencia guard:

guard <condición booleana> else {

    <código>

}

y que en la sección <código> sería donde bifurcaríamos la ejecución de nuestra aplicación. Una sección donde entre otros códigos podríamos especificar una de estas expresiones:

  • return
  • break
  • continue
  • throw

sin más, los problemas que teníamos con el código anterior los podemos corregir con algunas pocas modificaciones:

var nameEdit: UITextField?
var emailEdit: UITextField?
var passwordEdit: UITextField?
var confirmEdit: UITextField?

func registerOnTouch(sender: UIButton) {

    guard let name = nameEdit?.text, name != "" else {
        
        print("Especifique un nombre de usuario por favor!")
        
        return
        
    } // if

    guard let email = emailEdit?.text, email != "" else {
     
        print("Especifique un correo electrónico por favor!")
        
        return
        
    } // if
    
    guard let password = passwordEdit?.text, password != "" else {
    
        print("Escriba un password por favor!")
        
        return
        
    } // if

    guard let confirm = confirmEdit?.text, confirm != "" else {
     
        print("Confirme el password por favor!")
        
        return
        
    } // if

    if password != confirm {
     
        print("Ha fallado la confirmación.")
        
        return
        
    } // if

    print("Registrando nuevo usuario: \(name) con email: \(email) y password: \(password)")

} // registerOnTouch

nameEdit = UITextField()
emailEdit = UITextField()
passwordEdit = UITextField()
confirmEdit = UITextField()

nameEdit?.text = "josuevhn"
emailEdit?.text = "josuevhn@example.com"
passwordEdit?.text = "123"
confirmEdit?.text = "123"

registerOnTouch(sender: UIButton())

En un análisis rápido ya podemos percatarnos de que guard nos ayuda con la legibilidad al permitirnos vigilar por las condiciones que deseamos. Por ejemplo cuando especificamos:

guard let name = nameEdit?.text, name != ""

estaríamos expresando la intención de:

«vigilar la correcta creación de la constante name al desempaquetar nameEdit?.text al mismo tiempo que name no puede ser igual a «» y en caso de serlo pues ejecutaríamos el código dentro de las llaves, momento donde interrumpimos la ejecución de la función transfiriendo el control de flujo fuera de la misma.»

Pero la principal característica de guard, que también salta a la vista, es que podemos acceder a las constantes ya desempaquetadas y fuera del ámbito de sus llaves a diferencia de cuando usamos «if let«, algo que claramente nos da mucha más flexibilidad y nos ayuda a crear código más limpio y fácil de mantener.

Bucles

La instrucción guard también es útil cuando trabajamos con bucles. Veamos otro ejemplo:

let array = ["1", "2", "3", "4", nil, "6", "7", "8", "9"]

for item in array {

    if item == nil || Int(item!)! % 2 != 0 {
        
        continue
        
    } // if
    
    print("Hemos encontrado el valor par: \(item!)")

} // for

Hemos adoptado un enfoque de salida temprana (o en este caso de salto temprano ?), las primeras instrucciones que ejecutamos son aquellas que verifican si se cumplen o no los requisitos que hemos establecido, si estas devuelven true el ciclo actual se interrumpe y saltamos al siguiente.

Aunque el código anterior funciona, a mi modo de ver, la línea 5 no luce muy legible y quizás para algunos sí un poco críptica:  nuevamente estamos verificando por casos que no deseamos y estamos desempaquetando item al pasarlo como parámetro del inicializador de Int, al mismo tiempo que desempaquetamos el valor opcional que nos devuelve el inicializador de Int.

Al final cuando miramos la línea tenemos tantos signos (||, !, %, =) que si esta fuese un poco más larga ya sería bastante difícil de leer y entender de manera rápida, esto claramente no es legible y es difícil de mantener ya que dada su complejidad puede dar lugar a errores si no llevamos bien en la mente la lógica de cada uno de estos signos dentro de la expresión.

Veamos a continuación lo que a mi entender es una versión mucho más legible y clara:

let array = ["1", "2", "3", "4", nil, "6", "7", "8", "9"]

for item in array {
    
    let value: Int = {
        
        let newValue: Int? = Int(item ?? "0")
        
        return newValue ?? 0
        
    }()

    guard value != 0, (value % 2) == 0 else {
        
        continue
    
    } // guard

    print("Hemos encontrado el valor par: \(value)")
    
} // for

Sí, ya se que es mucho más largo que el anterior pero te aseguro que es un código mucho más estable, legible y fácil de mantener. Seguimos con el enfoque de salida temprana pero antes nos enfocamos en obtener un valor que podamos manejar con seguridad.

Para esto creamos una constante llamada value la cual igualamos con un closure (de la línea 5 a la 11). En este último creamos una constante opcional de tipo Int (línea 7) que igualamos al valor opcional entero que nos devuelve la expresión:

Int(item ?? "0")

En este inicializador hemos hecho uso del operador de coalescencia nil (??) con el cual declaramos nuestra intención de: si al desempaquetar item este contiene un valor válido se pasa ese valor al inicializador, por el contrario si es nil el valor que pasamos es la cadena «0» ya que recordemos que estamos trabajando con un arreglo de cadenas de texto o String.

Con esto nos aseguramos de que si item contiene un valor válido o nil la constante newValue siempre tendrá un valor con el que podremos trabajar.

Nota: Si no entiendes algunos de los conceptos que estamos comentando te recomiendo nuestro artículo sobre los valores opcionales y el operador de coalescencia nil, así como el dedicado a los closures.

Aun así hay una posibilidad de que newValue sea igual a nil y es cuando el valor de item no es nil pero tampoco es válido, es decir cuando el inicializador de Int no puede convertir la cadena String en un valor entero y esto sucedería cuando el valor almacenado en item contiene una letra o un símbolo.

Para solucionar esto hacemos una última verificación antes de retornar el valor de newValue (línea 9): usamos nuevamente el operador de coalescencia nil para en el caso de que sea nil retornar 0, de lo contrario su valor actual ya convertido a entero.

El closure lo cerramos con paréntesis () para que el código se ejecute y se almacene, el resultado, en la constante value.

Continuamos con la sentencia guard de la línea 13 a la 17, donde verificamos las condiciones que deseamos se cumplan para que nuestro código se ejecute y de no ser así, pues saltamos al siguiente ciclo del bucle.

Las condiciones vienen dadas porque sabemos que value puede ser igual a 0 (por lo antes explicado) siendo este el caso, lo descartamos y saltamos al siguiente ciclo ya que no es un número par.

En el caso de que sea cualquier otro número pasamos a verificar su paridad, si esto también devuelve un resultado afirmativo quiere decir que ha sido validado y puede ser imprimido en pantalla como un número par.

Si modificamos el array a los siguientes valores:

let array = ["1", "*", "3", "4", nil, "6", "N", "8", "9"]

la salida en pantalla sería:

Hemos encontrado el valor par: 4
Hemos encontrado el valor par: 6
Hemos encontrado el valor par: 8

Como podemos constatar no hay errores, ni el símbolo de asterisco (*), el valor nil o la letra N pudieron detener la correcta ejecución de nuestro código.

Conclusiones

Como hemos visto en los ejemplos la sentencia guard es muy útil para ciertos casos y como su propio nombre lo indica, algo que ya de por sí ayuda con la legibilidad, su enfoque está en la validación, en servirnos de guardian para que si nuestras condiciones no se cumplen el código dentro de cierta función o bucle jamás se ejecuten.

Hemos visto también como se diferencia de «if let» dándonos la posibilidad de acceder a constantes ya desempaquetadas y fuera del ámbito de las llaves. Creo que ha quedado claro que guard e if existen para afrontar situaciones distintas.

Falta aún mucho por aprender en nuestro camino a convertirnos en iOS Developer. Suscríbete a nuestra lista de correo y síguenos en nuestras redes sociales. Mantente al tanto de todas nuestras publicaciones.

Espero que todo cuanto se ha dicho aquí, de una forma u otra le haya servido de aprendizaje, de referencia, que haya valido su preciado tiempo.

Este artículo, al igual que el resto, será revisado con cierta frecuencia en pos de mantener un contenido de calidad y actualizado.

¡Cualquier duda o sugerencia, ya sea errores a corregir o ejemplos a añadir, será más que bienvenida, necesaria!

Deja una respuesta

Su dirección de correo electrónico no será publicada.

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.

Este sitio web utiliza cookies para mejorar su experiencia. Asumiremos que está de acuerdo con esto, pero puede optar por no participar si lo desea. Aceptar Leer Más

RECIBE CONTENIDO SIMILAR EN TU CORREO

RECIBE CONTENIDO SIMILAR EN TU CORREO

Suscríbete a nuestra lista de correo y mantente actualizado con las nuevas publicaciones.

Se ha suscrito correctamente!