diegoRodicio

Está documentación está a túa disposición sin ningún custo económico. Sen embargo, para a súa elaboración dedico moito tempo e recursos, polo que agradecería unha colaboración co que consideres oportuno. Gracias.

Closures ou peches

Os closures (peches) en Swift son bloques de funcionalidade que realizan unha tarefa, e son moi parecidos ás funcións.

Un closure é como unha función anónima (sen nome) onde temos un body e dentro do cal executamos o noso código.

O seguinte código, por exemplo, declara unha expresión de peche e asígnaa a unha constante chamada diAlmendrado e logo chama á función:

let diAlmendrado = { print("Almendrado") }
diAlmendrado()

Parámetros e valores de devolución nos peches #

Os closures tamén se poden configurar para aceptar parámetros e devolver resultados.

Vamos a ver como, no seguinte exemplo, un closure acepta dous parámetros enteiros e devolve un resultado enteiro:

let multiplicar = {(_ val1: Int, _ val2: Int) -> Int in
  return val1 * val2
}
let resultado = multiplicar(10, 20)

Sintaxe dun Closure #

A continuación explicamos liña por liña:

{ 
    (parametro: Int) -> Int in                          // 1
    print("O valor do parámetro é: \(parametro)")       // 2
    return parametro                                    // 3
}                                                       // 4

  • Liña 1: Para crear un closure abrimos chaves { e creamos o seu body (scope). Ao abrir as chaves especificamos se o noso closure acepta parámetros de entrada, e tamén especificamos se retorna un valor. No noso exemplo, o closure acepta un parámetro de entrada de tipo Int e retorna un valor de tipo Int.
  • Liña 2: Dentro do scope do noso closure engadimos a lóxica que queremos realizar. No noso caso só queremos amosar unha mensaxe por consola.
  • Liña 3: Retornamos o valor de tipo Int. Neste caso retornamos o mesmo parámetro de entrada que lle pasamos ao closure.
  • Liña 4: Pechamos as chaves } indicando que aquí acaba o noso closure.

Se tentamos compilar o código anterior no noso Playground, obtemos un erro do compilador: Os closures deben de asignarse a unha constante ou variable, así que para eliminar o erro do compilador imos asignar o closure a unha constante chamada closure. Unha vez asignadas, chamamos ao noso closure pasándolle como parámetro de entrada un Int:

let closure = { 
    (parametro: Int) -> Int in                          // 1
    print("O valor do parámetro é: \(parametro)")       // 2
    return parametro                                    // 3
}   

closure(2)

// RESULTADO 👇
// O valor do parámetro é: 2 

Equivalencia con funcións #

Chamar a un closure é similar ao chamar a unha función. Escribimos o nome da constante ou variable e abrimos paréntese para pasarlle os parámetros de entrada.

Se o closure retorna un valor, podemos asignarllo a unha constante ou variable (o mesmo que fariamos ao usar unha función)

Así, poderiamos substituír o closure anterior pola seguinte función:

func closure (parametro: Int) -> Int {
    print("O valor do parámetro é: \(parametro)")       
    return parametro 
}

Optimizando Closures #

Imos crear un closure que non acepta ningún parámetro de entrada nin retorna ningún valor:

let closure = { () -> Void in
    print("Ven ao IES San Mamede!")
}

Void é un tipo especial en Swift que indica que non se retorna ningún valor. E neste caso ao non retornar ningún valor podemos eliminar a keyword Void e simplificar o noso código:

let closure = { () in
    print("Ven ao IES San Mamede!")
}

Tampouco temos parámetros de entrada, poderíamos facer outra optimización:

let closure = {
    print("Ven ao IES San Mamede!")
}

Fíxache como fomos simplificando o noso closure. Eliminamos información que era redundante para o compilador e o noso código quedou moito máis limpo.

Agora se queremos chamar ao noso closure, non basta con engadir no Playground closure, debemos engadir as parénteses ( ) para indicar que queremos executar a súa implementación:

let closure = {
    print("Ven ao IES San Mamede!")
}

closure()
//Imprime: Ven ao IES San Mamede!

Exemplo práctico sorted(by:) #

Imos ver outro exemplo de como optimizar o noso closure, neste caso imos usar o método sorted(by:) que podemos usar para ordenar un Array.

  • O método sorted(by:) acepta un closure que espera dous parámetros do mesmo tipo e retorna un Bool.
  • O booleano será true se o primeiro valor debe aparecer antes que o segundo valor
  • Será false en caso contrario.

Para ver un exemplo práctico, primeiro imos crear o Array e logo imos usar sorted(by:):

var frameworks = ["SwiftUI", "Combine", "UIKit", "Foundation"]”

frameworks.sorted { (primeiroValor, segundoValor) -> Bool in
    return primeiroValor < segundoValor
}

print(frameworks)
// RESULTADO 👇
// ["Combine", "Foundation", "SwiftUI", "UIKit"]”

Ao usar sorted(by:) enchemos o closure coa lóxica que queremos usar para ordenar o noso Array. Neste caso ordenámolo alfabeticamente (é dicir, ordénase o Array da Á a Z).

Aínda así, podemos seguir optimizando o noso closure, xa que podemos eliminar a keyword return.

Isto é posible cando o noso closure (ou función) retorna un tipo e dentro do closure só temos 1 liña de código.

O noso closure quedaría da seguinte maneira:

var frameworks = ["SwiftUI", "Combine", "UIKit", "Foundation"]”

frameworks.sorted { (primeiroValor, segundoValor) -> Bool in
    primeiroValor < segundoValor
}

print(frameworks)
// RESULTADO 👇
// ["Combine", "Foundation", "SwiftUI", "UIKit"]”

Podemos optimizar aínda máis o noso closure. Para simplificar os parámetros de entrada que entran ao noso closure podemos usar $0, $1, $2, etc. Neste caso $0 representa o parámetro que antes chamabamos primeiroValor, e $1 representar o parámetro que antes chamabamos segundoValor.

frameworks.sorted { $0 < $1 }

print(frameworks)
// RESULTADO 👇
// ["Combine", "Foundation", "SwiftUI", "UIKit"]”

Incluso aínda o podemos simplificar máis!!:

frameworks.sorted(by: <)

Acabamos de ver un exemplo moi potente e práctico.
Fomos reducindo considerablemente o código do noso closure.

Todas as optimizacións devolven o mesmo resultado, pero que optimización é a mellor? no meu caso prefiro usar sempre a versión máis simplificada xa que engadindo menos código é máis probable que engadas menos bugs.

Pero ás veces terás que priorizar se engadir menos código ou engadir máis código para que sexa máis fácil de entender e ler (en resumo: hai que buscar un equilibrio).

Substituír Closures por funcións #

En ocasións a lóxica que se engade a un closure pode extraerse nunha función.

Así, usaremos unha función chamada ordenar, que ten o mesmo tipo que o closure, e que ordena dúas String e retorna un Bool. Como vemos no seguinte código, a función ordear ten exactamente a mesma lóxica que o que usamos dentro do closure de sorted(by: ):

var frameworks = ["SwiftUI", "Combine", "UIKit", "Foundation"]

frameworks.sorted { (primeiroValor, segundoValor) -> Bool in
    primeiroValor < segundoValor
}

func ordear(_ cadea1: String, _ cadea2: String) -> Bool {
    cadea1 < cadea2
}

Agora, podemos substituír a lóxica que temos no closure e pasar como parámetro a función ordear, é dicir poderiamos facer o seguinte cambio:

var frameworks = ["SwiftUI", "Combine", "UIKit", "Foundation"]

func ordear(_ cadea1: String, _ cadea2: String) -> Bool {
    cadea1 < cadea2
}

var frameworksOrdeadas = frameworks.sorted(by: ordear)

print(frameworksOrdeadas)
// RESULTADO 👇
// ["Combine", "Foundation", "SwiftUI", "UIKit"]”

O resultado é bastante limpo, en lugar de ter un closure con toda a lóxica, o que fixemos foi substituír o closure por unha función.
Este cambio foi posible xa que o closure espera que lle pasemos 2 parámetros de tipo String e que retorne un Bool, e é xusto o tipo da nosa función ordear.

Para deixalo máis claro, o tipo que espera o closure de sorted(by:) é:

(String, String) -> Bool

O mesmo que o da función ordear:

(String, String) -> Bool

Closures como parámetros de funcións: Trailing Closures #

Ao crear unha función podemos pasar parámetros de entrada para que se usen dentro da súa scope. Estes parámetros tamén poden ser closures, e se enviamos un closure como último parámetro dunha función a este chámaselle trailing closures.

O uso de trailing closures fai que o teu código sexa máis fácil de ler.

Un closure como parámetro #

Imos crear unha función que acepte un closure como parámetro:

func createUser(nome: String, closure: (String, String) -> Void) {
    print("Crear usuario: \(nome)")
    closure(nome, "Suscríbete")
    print("Completado")
}

Neste exemplo, creamos 2 parámetros de entrada. O primeiro é un String e o segundo é un closure (ou función) que acepta dous tipos String.

Cando chamemos ao closure podemos implementar a lóxica que queiramos, no meu caso engadín un print:

func createUser(nome: String, closure: (String, String) -> Void) {
    print("Crear usuario: \(nome)")
    closure(nome, "Suscríbete")
    print("Completado")
}

createUser(nome: "drodicio") { username, action in
    print("Log: Crear o usuario: \(username) con: \(action)")
}

//Imprime: Log: Crear o usuario: drodicio con: Suscríbete

Agora imaxina que temos a seguinte función cun único parámetro de entrada e este parámetro de entrada é un closure:

func removeAllUsers(closure: (String, String) -> Void) {
    print("Eliminar TODOS os usuarios")
    closure("Usuarios", "BaseDatos")
    print("Completado")
}

Agora, podemos chamar a función do seguinte xeito:

func removeAllUsers(closure: (String, String) -> Void) {
    print("Eliminar TODOS os usuarios")
    closure("Usuarios", "BaseDatos")
    print("Completado")
}

removeAllUsers { nome, localizacion in
    print("Log: Eliminar tabla \(nome) in \(localizacion)")
}
//Imprime: Log: Eliminar tabla Usuarios in BaseDatos

Varios closures como parámetros #

Ao crear unha función podemos pasar closures como parámetros de entrada. Isto é moi útil para crear varios camiños dentro do teu código, agora imos ver un exemplo moi sinxelo.

Imos simular que realizamos unha petición HTTP e segundo o resultado obtido executamos un closure ou outro. É dicir, no seguinte código, se todo vai ben executamos onSuccess e se hai un erro executamos onFailure.

func obterDatos(status: String,
                        onSuccess: () -> Void,
                        onFailure:(String) -> Void) {
    if status == "OK" {
        onSuccess()
    } else {
        onFailure(status)
    }
}

obterDatos(status: "OK") {
    print("Success")
} onFailure: { status in
    print("Error: \(status)")
}
// RESULTADO 👇
// Success”


Ao compilar o noso Playground obtemos unha mensaxe por consola de Success. Se modificamos o parámetro de entrada por «KO» obtemos a mensaxe de erro, é dicir, estamos a executar o closure onFailure.

Ao pasar diferentes closures como parámetros a unha función podemos executar diferentes camiños dependendo da lóxica que apliquemos

Share Your Valuable Opinions