Listas de captura rápida: ¿cuál es la diferencia entre enlaces débiles, fuertes y no propios?


Joseph Wright, The Prisoner - Ilustración de una captura fuerte

La lista de valores "capturados" se encuentra frente a la lista de parámetros de cierre y puede "capturar" valores del alcance de tres maneras diferentes: utilizando los enlaces "fuerte", "débil" o "sin dueño". A menudo lo usamos, principalmente para evitar ciclos de referencia fuertes ("ciclos de referencia fuertes" también conocidos como "ciclos de retención").
Puede ser difícil para un desarrollador novato decidir qué método aplicar, por lo que puede pasar mucho tiempo eligiendo entre "fuerte" y "débil" o entre "débil" y "sin dueño", pero, con el tiempo, se dará cuenta de que la elección correcta - solo uno.

Primero, crea una clase simple:

class Singer { func playSong() { print("Shake it off!") } } 

Luego, escribimos una función que crea una instancia de la clase Singer y devuelve un cierre que llama al método playSong () de la clase Singer :

 func sing() -> () -> Void { let taylor = Singer() let singing = { taylor.playSong() return } return singing } 

Finalmente, podemos llamar a sing () en cualquier lugar para obtener el resultado de jugar playSong ()

 let singFunction = sing() singFunction() 


Como resultado, se mostrará la línea "Shake it off!".

Captura fuerte


A menos que especifique explícitamente un método de captura, Swift utiliza una captura "fuerte". Esto significa que el cierre captura los valores externos utilizados y nunca los dejará libres.

Echemos un vistazo a la función sing () nuevamente

 func sing() -> () -> Void { let taylor = Singer() let singing = { taylor.playSong() return } return singing } 

La constante taylor se define dentro de una función, por lo que, en circunstancias normales, su lugar se liberaría una vez que la función haya terminado su trabajo. Sin embargo, esta constante se usa dentro del cierre, lo que significa que Swift asegurará automáticamente su presencia mientras exista el cierre, incluso después del final de la función.
Esta es una captura "fuerte" en acción. Si Swift permitió que se liberara a taylor , entonces llamar a un cierre no sería seguro: su método taylor.playSong () ya no es válido.

Captura "débil" (captura débil)


Swift nos permite crear una " lista de captura " para determinar cómo se capturan los valores utilizados. Una alternativa a la captura "fuerte" es "débil" y su aplicación conlleva las siguientes consecuencias:

1. Los valores capturados "débilmente" no se mantienen en el cierre y, por lo tanto, pueden liberarse y establecerse en nulo .

2. Como consecuencia del primer párrafo, los valores capturados "débilmente" en Swift son siempre opcionales .
Modificamos nuestro ejemplo usando una captura "débil" e inmediatamente vemos la diferencia.

 func sing() -> () -> Void { let taylor = Singer() let singing = { [weak taylor] in taylor?.playSong() return } return singing } 

[taylor débil] : esta es nuestra " lista de captura ", una parte especial de la sintaxis de cierre en la que damos instrucciones sobre cómo se deben capturar los valores. Aquí decimos que taylor debe capturarse "débilmente", por lo que debemos usar taylor? .PlaySong () : ahora es opcional , ya que puede establecerse en nulo en cualquier momento.

Si ahora ejecuta este código, verá que llamar a singFunction () ya no da como resultado un mensaje. La razón de esto es que taylor existe solo dentro de sing () , y el cierre devuelto por esta función no mantiene a taylor "fuertemente" dentro de sí mismo.

Ahora intenta cambiar taylor? .PlaySong () a taylor! .PlaySong () . Esto conducirá a un desempaque forzado de taylor dentro del cierre y, en consecuencia, a un error fatal (desempaquetar contenido que contenga cero )

Captura "sin propietario" (captura sin propietario)


Una alternativa a la captura "débil" es "sin propietario".

 func sing() -> () -> Void { let taylor = Singer() let singing = { [unowned taylor] in taylor.playSong() return } return singing } 

Este código terminará anormalmente de manera similar con el opcional desplegado opcionalmente que se muestra arriba: Taylor no propietario dice: "Sé con certeza que Taylor existirá durante el cierre, por lo que no necesito guardarlo en la memoria". De hecho, Taylor se lanzará casi de inmediato y este código se bloqueará.

Por lo tanto, use sin propietario con mucho cuidado.

Problemas comunes


Hay cuatro problemas que enfrentan los desarrolladores cuando usan la captura de valor en los cierres:

1. Dificultades con la ubicación de la lista de captura en el caso en que el cierre toma parámetros


Este es un problema común que puede encontrar al comienzo del estudio de cierres, pero, afortunadamente, Swift nos ayudará en este caso.

Al usar la lista de captura y los parámetros de cierre juntos, la lista de captura viene entre corchetes, luego los parámetros de cierre, luego la palabra clave in, que marca el comienzo del "cuerpo" de cierre.

 writeToLog { [weak self] user, message in self?.addToLog("\(user) triggered event: \(message)") } 

Intentar poner una lista de captura después de los parámetros de cierre dará como resultado un error de compilación.

2. La aparición de un ciclo de enlaces fuertes, que conduce a una pérdida de memoria


Cuando una entidad A tiene una entidad B, y viceversa, tiene una situación llamada "ciclo de retención".

Como ejemplo, considere el código:

 class House { var ownerDetails: (() -> Void)? func printDetails() { print("This is a great house.") } deinit { print("I'm being demolished!") } } 

Definimos la clase House , que contiene una propiedad (cierre), un método y un inicializador que mostrará un mensaje cuando se destruya una instancia de la clase.

Ahora cree una clase de Propietario similar a la anterior, excepto que su propiedad de cierre contiene información sobre la casa.

 class Owner { var houseDetails: (() -> Void)? func printDetails() { print("I own a house.") } deinit { print("I'm dying!") } } 

Ahora cree instancias de estas clases dentro del bloque do . No necesitamos un bloque catch, pero el uso de un bloque do destruirá las instancias justo después}

 print("Creating a house and an owner") do { let house = House() let owner = Owner() } print("Done") 

Como resultado, se mostrarán mensajes: "Creando una casa y un propietario", "¡Me estoy muriendo!", "¡Estoy siendo demolido!", Y luego "Hecho": todo funciona como debería.

Ahora crea un bucle de enlaces fuertes.

 print("Creating a house and an owner") do { let house = House() let owner = Owner() house.ownerDetails = owner.printDetails owner.houseDetails = house.printDetails } print("Done") 

Ahora aparecerá el mensaje "Creando una casa y un propietario", luego "Listo". No se llamarán desinfectantes.

Esto sucedió como resultado del hecho de que la casa tiene una propiedad que apunta al propietario, y el propietario tiene una propiedad que apunta a la casa. Por lo tanto, ninguno de ellos puede liberarse de forma segura. En una situación real, esto conduce a pérdidas de memoria, que conducen a un bajo rendimiento e incluso a un bloqueo de la aplicación.

Para solucionar la situación, necesitamos crear un nuevo cierre y usar una captura "débil" en uno o dos casos, como este:

 print("Creating a house and an owner") do { let house = House() let owner = Owner() house.ownerDetails = { [weak owner] in owner?.printDetails() } owner.houseDetails = { [weak house] in house?.printDetails() } } print("Done") 

No es necesario declarar ambos valores capturados, es suficiente hacerlo en un solo lugar; esto permitirá que Swift destruya ambas clases cuando sea necesario.

En proyectos reales, la situación de un ciclo tan obvio de vínculos fuertes rara vez surge, pero esto habla aún más de la importancia de utilizar la captura "débil" con un desarrollo competente.

3. El uso inadvertido de enlaces fuertes, generalmente al capturar múltiples valores


Swift utiliza un fuerte agarre por defecto, lo que puede conducir a un comportamiento inesperado.
Considere el siguiente código:

 func sing() -> () -> Void { let taylor = Singer() let adele = Singer() let singing = { [unowned taylor, adele] in taylor.playSong() adele.playSong() return } return singing } 

Ahora tenemos dos valores capturados por el cierre, y los usamos a ambos de la misma manera. Sin embargo, solo taylor se captura como no propiedad: se captura a adele con fuerza porque la palabra clave no propiedad debe usarse para cada valor capturado.

Si hizo esto a propósito, entonces todo está bien, pero si desea que ambos valores se capturen " sin propiedad ", necesita lo siguiente:

 [unowned taylor, unowned adele] 

4. Copie los cierres y comparta los valores capturados


El último caso en el que los desarrolladores se topan es cómo se copian las fallas porque los datos que capturan están disponibles para todas las copias de la falla.
Considere un ejemplo de un cierre simple que captura la variable entera numberOfLinesLogged declarada fuera del cierre, para que podamos aumentar su valor e imprimirlo cada vez que se llama al cierre:

 var numberOfLinesLogged = 0 let logger1 = { numberOfLinesLogged += 1 print("Lines logged: \(numberOfLinesLogged)") } logger1() 

Esto mostrará el mensaje "Líneas registradas: 1".
Ahora crearemos una copia del cierre que compartirá los valores capturados junto con el primer cierre. Por lo tanto, si llamamos al cierre original o su copia, veremos el valor creciente de la variable.

 let logger2 = logger1 logger2() logger1() logger2() 

Esto imprimirá los mensajes "Líneas registradas: 1" ... "Líneas registradas: 4" porque logger1 y logger2 apuntan a la misma variable numberOfLinesLogged capturada.

Cuándo usar una captura "fuerte", "débil" y "sin dueño"


Ahora que entendemos cómo funciona todo, intentemos resumir:

1. Si está seguro de que el valor capturado nunca será nulo al realizar el cierre, puede usar "captura no propiedad" . Esta es una situación infrecuente en la que el uso de la captura "débil" puede causar dificultades adicionales, incluso cuando se usa la protección de dejar un valor débilmente capturado dentro del cierre.

2. Si tiene un caso de un ciclo de enlaces fuertes (la entidad A es propietaria de la entidad B y la entidad B es propietaria de la entidad A), entonces, en uno de los casos, debe usar "captura débil" . Es necesario tener en cuenta cuál de las dos entidades se liberará primero, por lo que si el controlador de vista A representa el controlador de vista B, entonces el controlador de vista B puede contener un enlace "débil" de regreso a "A".

3. Si se excluye la posibilidad de un ciclo de enlaces fuertes, puede usar la captura "fuerte" ( "captura fuerte" ). Por ejemplo, ejecutar una animación no se bloquea dentro del cierre que contiene la animación, por lo que puede usar un enlace fuerte.

4. Si no está seguro, comience con un enlace "débil" y cámbielo solo si es necesario.

Opcional - Guía oficial de Swift:
Cortocircuitos
Conteo automático de enlaces

Source: https://habr.com/ru/post/444336/


All Articles