Todo lo que jamás quisiste saber sobre los bloques de Objective-C

Autor: | Última modificación: 26 de septiembre de 2023 | Tiempo de Lectura: 4 minutos
Temas en este post: ,

Todos sabemos lo que es una FAQ: Frequently Asked Questions; uséase, preguntas formuladas frecuentemente. No obstante, a menudo las respuestas más interesantes son las que corresponden a preguntas formuladas infrecuentemente.

Veamos un par de ellas relacionadas con los bloques de Objective C:

¿Por qué las propiedades de bloques se deben de declarar como copy? ¿Por qué puñetas hay que copiar los bloques?

o bien,

¿Por qué cuando declaras un bloque, no añades un asterisco como con todos los objetos de Objective C? ¿Acaso los bloques no son objetos alojados en el heap (montículo) y manipulados mediante punteros?

Para responder a esto, tenemos que repasar algunos hechos sobre los bloques de Objective C.

Bloques de Objective-C

Captura de variables

Las variables, cuando son capturadas, lo son como copias constantes.  Esto se hace así para protegerlas de posteriores cambios. No importa si posteriormente alguien cambia el valor de una variable ya capturada por el bloque. Para el bloque, ésta siempre tendrá el valor que tenía cuando fue capturada por el mismo. A esto se le llama captura léxica.

Si ejecutamos el siguiente código, veremos que los valores que devuelve cada bloque son 1, 2 y 3.

// Un typedef para el bloque
// www.putasintaxisdebloques.com
typedef void(^Thunk)(void);

-(void) captureVars{

    int i = 1;
    Thunk block1 = ^{NSLog(@"%d %p",i, &i);};

    i = 2;
    Thunk block2 = ^{NSLog(@"%d %p",i, &i);};

    i = 3;
    Thunk block3 = ^{NSLog(@"%d %p",i, &i);};

    block1();
    block2();
    block3();

}

Por si fuera poco,  vemos que la dirección de memoria en la que está i es distinta en cada caso: la variable ha sido copiada a otra zona de memoria.

La salida es:

2014-02-20 13:11:00.764 TestBlocks[5704:70b] 1 0xbfffcaf4
2014-02-20 13:11:01.743 TestBlocks[5704:70b] 2 0xbfffcad4
2014-02-20 13:11:02.501 TestBlocks[5704:70b] 3 0xbfffcab4

La captura léxica, al igual que casi todo, ha sido inventada por la comunidad de Lisp (Santo, Santo, Santo) y llevada a los mortales por Smalltalk.

En el caso de ser objetos, estos son retenidos, así que no hay riesgo que sea destruido sin que el bloque se entere.

Los programadores funcionales están contentos y satisfechos al asignar una sola vez una variable y no cambiar su valor jamás. No obstante, nosotros somos herejes, inconscientes de la Luz y la Sabiduría del Cálculo Lambda (No hay más Dios que Alonzo Church y John McCarthy su único profeta), y nos regozijamos en el error de reasignar variables.

john-mccarthy_bloques de Objective-C
John McCarthy, creador de Lisp: manantial de sabiduría y paréntesis sin sin fin.

Para esos casos, tenemos la palabra clave __block, que cambia como se almacena una variable capturada. Si a la variable i la marcamos como __block, el resultado será muy distinto:

-(void) blockVars{

    __block int i = 1;
    Thunk block1 = ^{NSLog(@"%d %p",i, &i);};

    i = 2;
    Thunk block2 = ^{NSLog(@"%d %p",i, &i);};

    i = 3;
    Thunk block3 = ^{NSLog(@"%d %p",i, &i);};

    NSLog(@"%d %p",i, &i);

    block1();
    block2();
    block3();

}

y la salida va y resulta que es:

2014-02-20 13:30:17.594 TestBlocks[5813:70b] 3 0xbfffcb00
2014-02-20 13:30:17.595 TestBlocks[5813:70b] 3 0xbfffcb00
2014-02-20 13:30:17.595 TestBlocks[5813:70b] 3 0xbfffcb00
2014-02-20 13:30:17.596 TestBlocks[5813:70b] 3 0xbfffcb00

Es decir, todo Jobs accede a la misma variable, en la misma dirección de memoria con el consiguiente riesgo de despelote.

__block hace que las variables se compartan.

En el caso de objetos, éstos NO son retenidos, y podrían desaparecer sin que el bloque se entere.

Como no podría ser de otra forma, este error/horror también fue creado por Lisp en sus primeras versiones y se le llama captura dinámica. Posteriormente, fue sustituido por la captura léxica en todos los dialectos civilizados de Lisp.

Los bloques son objetos

Los bloques son objetos, pero unos objetos algo rarillos. Para empezar, se almacenan parcialmente en la pila (stack) en vez del montículo (heap) como los demás objetos. El código en sí, se almacena en el montículo, pero las variables capturadas se dejan en la pila.

Esto se hace así porque guardar en la pila es mucho más rápido. Sin embargo, ¿qué pasa cuando un bloque se usa fuera del entorno en que ha sido creado? Sus variables capturadas apuntarán a zonas de memoria que ya no son válidas, y al usuario la aplicación se caerá.

¿Cómo podemos evitar esto? Haciendo una copia del bloque. La primera vez que se copia, se llevan los valores de las variables al montículo. Las demás veces son simplemente un retain.

Todo esto es válido cuando no estás usando ARC. En este caso, los bloques son por defecto instancias de __NSStackBlock (clase oculta que representa un bloque en la pila).

Sin embargo, si usas ARC, los bloques son por defecto instancias de __NSMallocBlock (clase oculta que representa un bloque en el montículo). En este caso todo está en el montículo y ARC se encarga de hacer la copia siempre que haga falta.

¿Por qué las propiedades de bloques se deben de declarar como copy? ¿Por qué puñetas hay que copiar los bloques?

Ya sabemos por qué hay que copiar los bloques, porque si no lo hacemos, sus variables capturadas apuntarán a donde no deben una vez que el bloque esté fuera del contexto donde ha sido creado. Ahora bien, si usamos ARC, no hace falta preocuparse por esto, ya que ARC copiará los bloques siempre que haga falta, no sólo en el setter de una propiedad.

Por lo tanto, si usas ARC, no tiene ningún sentido declarar una propiedad de tipo bloque de esta forma:

// Un typedef para el bloque
// www.putasintaxisdebloques.com
typedef void(^Thunk)(void);

@interface AGTAppDelegate : UIResponder <UIApplicationDelegate>

@property (copy, nonatomic) Thunk myBlock;

@property (strong, nonatomic) UIWindow *window;
@end

Tiene mucho más sentido declararla como cualquier otro objeto:

// Un typedef para el bloque
// www.putasintaxisdebloques.com
typedef void(^Thunk)(void);
@interface AGTAppDelegate : UIResponder <UIApplicationDelegate>

@property (strong, nonatomic) Thunk myBlock;

@property (strong, nonatomic) UIWindow *window;
@end

Ahora bien, por qué puñetas escribo

@property (strong, nonatomic) Thunk myBlock;

en vez de

@property (strong, nonatomic) Thunk *myBlock;

¿Por qué cuando declaras un bloque, no añades un asterisco como con todos los objetos de Objective C?

¿Acaso los bloques no son objetos? Pues sí que lo son, y se suelen almacenar en el montículo igual que los demás. ¿Entonces por qué no accedemos a ellos mediante un puntero, como todos los demás objetos?

Realmente accedemos a ellos mediante un puntero, aunque no pongamos un asterisco. La sintaxis typedef void(^Thunk)(void);  no declara Thunk como un bloque, sino como un puntero a un bloque.

La falta del asterisco es tan sólo un truco del compilador para ocultarte los aspectos más chungos de los bloques.

Si estas cuestiones te han provocado dolores de cabeza, vas por buen camino. El secreto del éxito es aprender, aprender y aprender, no hay fórmula mágica.