¿Sabes cuál es la diferencia entre los @escaping y los @nonescaping closures en Swift?
¿Podrías explicarlo?
En este artículo te cuento cuáles son las diferencias entre @escaping y @nonescaping en Swift y por qué es importante para el lenguaje hacer esta distinción. Todo ello utilizando ejemplos que ayuden a comprender las diferencias.
Si te preguntas por qué no has tenido que escribir nunca @nonescaping
en un closure, no te preocupes. Ocurre porque, desde Swift 3, todos los closures son @nonescaping por defecto, por lo que no hay que ponerlo explícitamente.
¿Todavía no sabes lo que es un closure? : es un bloque anónimo de código que puede ser asignado a una variable o pasado como parámetro dentro de una función. Visita «Los closures en Swift» para más información.
Diferencia entre @escaping Swift y @nonescaping:
Cuando utilizas un closure como parámetro a una función, existen ocasiones donde el compilador te exige escribir la keyword @escaping delante de su definición.
Por lo tanto, si existen los @escaping swift closures, tendrán que existir los @nonescaping… ¿no?
Vamos a crear un closure que acepte un número entero y no devuelva nada: (Int) -> Void
// Una función que acepta un parámetro entero
// y no devuelve nada, es un closure (Int) -> Void
func add42(to number: Int) {
print("Result: \(number + 42)")
}
print("Begin")
add42(to: 8)
print("End")
Por la consola de Xcode se imprime lo siguiente:
"Begin"
"Result: 50"
"End"
Utilizaremos este closure/función add42
para ejemplificar closures @escaping y @nonescaping:
· @nonescaping
Swift:
Los closures @nonescaping son aquellos que se ejecutan inmediatamente dentro del ámbito (o scope, en inglés) de la función.
Es decir, aquellos bloques de código que se llaman y ejecutan antes de que la función termine de ejecutarse.
Dicho de otro modo, el closure «no se escapa» de la función, «no vive» más allá de la función que lo contiene.
func apply(_ function: (Int) -> Void, to number: Int) {
function(number)
}
print("Begin")
apply(add42, to:8)
print("End")
El código del closure function
es @nonescaping y se llama antes de que el programa termine de ejecutar la función apply
, por lo que en la consola de Xcode se imprime:
"Begin"
"Result: 50"
"End"
· @escaping
Swift:
Los closures @escaping son aquellos que pueden ser ejecutados o llamados más tarde, después de que la función que los contiene haya terminado de ejecutarse.
Dicho de otro modo, el closure «se escapa» del ámbito de la función.
Vamos a crear una función que ejecuta el closure después de cinco segundos, emulando una tarea asíncrona en segundo plano:
func applyInBackground(_ function: @escaping (Int) -> Void, with number: Int) {
DispatchQueue.main.asyncAfter(deadline: .now() + 5.0) {
function(number)
}
}
print("Begin")
applyInBackground(add42, with: 8)
print("End")
El closure function
es @escaping, es decir se escapa del ámbito de la función y se llama más tarde de que la función haya retornado, por lo que en la consola de Xcode se imprime:
"Begin"
"End"
"Result: 50" (tras 5 segundos)
Puedes sacar la conclusión correcta de que todo bloque de código asíncrono debe marcarse como @escaping.
¿Por qué es importante distinguirlos?
Swift necesita saber si el closure va a ser ejecutado inmediatamente o si por el contrario, necesita ser almacenado para ejecutarse más tarde. Los closures son tipos por referencia en Swift, por lo tanto utilizan ARC para gestionar la memoria.
Cuando el closure va a ser utilizado inmediatamente (@nonescaping), el compilador no aumenta en +1
su retainCount
, ya que se va a ejecutar directamente y así puede hacer una serie de mejoras en el rendimiento.
Pero si se ejecuta más tarde (@escaping), el compilador tiene que añadir +1
a su retainCount
para almacenarlo y que no sea eliminado de memoria.
Siempre que el closure tenga que ser almacenado, éste debe ser @escaping.
Además de los closures que se pasan como parámetro, otro ejemplo de tipo @escaping ocurre cuando un objeto almacena una propiedad de tipo closure.
Imagina que tenemos un botón personalizado con una propiedad de tipo closure () -> Void
que almacena la acción que se ejecuta cuando se pulsa el botón:
class CustomButton: UIButton {
let tapAction: () -> Void
init(tapAction: @escaping () -> Void) {
self.tapAction = tapAction
super.init(frame: .zero)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
...
}
Fíjate en el método init
. El compilador te obliga a declarar el parámetro tapAction
como @escaping, ya que éste debe sumar +1
a su cuenta de referencias para almacenarlo y poder ejecutarlo en el futuro. El closure «se escapa» del setter de la propiedad.
Desde Swift 3.x, por defecto todos los closures son @nonescaping ya que es mucho más eficiente computacionalmente.
Curiosidades
Los closures capturan el entorno léxico que les rodea. Esto significa que capturan las constantes y variables que lo rodean, entre ellos self
. Por eso es muy fácil crear una referencia circular fuerte cuando utilizas closures.
Swift te obliga a utilizar explícitamente self
dentro de los closures tipo @escaping para forzarte a pensar sobre potenciales referencias circulares y resolverlas manualmente utilizando «capture lists«.
Sin embargo, es imposible crear una referencia circular con closures tipo @nonescaping, ya que el compilador puede garantizar que el closure libera todos los objetos capturados al salir de la función. Por eso, sólo se obliga a usar explícitamente self
en closures @escaping.
Únete a la newsletter para no perderte ningún nuevo tutorial: recibe un email cuando se publiquen nuevos artículos o vídeos y no pierdas la oportunidad de seguir aprendiendo.
Conclusión
Es importante entender las diferencias entre closures @escaping y @nonescaping en Swift. Principalmente, depende de cómo se gestiona la memoria de los bloques de código y de si se llaman dentro de la función que los contiene o no.
Lo bueno es que el compilador siempre te lanzará un error cuando éstos tengan que ser @escaping, así que difícilmente cometerás errores en este aspecto.
Espero que te haya resultado útil este artículo. Si tienes cualquier duda, no dudes en dejarme un comentario o escribirme en mi Twitter o en la página de Facebook del blog.
Por último, ¿me haces un favor compartiendo el post en tus redes?
Gracias 🙂