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 tipoInt
e retorna un valor de tipoInt
. - 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 unBool
. - 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