Bloques en Objective C y Cocoa

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

Bloques en Objective C y Cocoa

Bloques en Objective C

La principal fuente de inspiración de Objective C ha sido Smalltalk. Sin embargo, una de las características más interesantes y originales de Smalltalk seguía faltando: los bloques. Eso hasta hace poco, porque en la especificación de 2.0 de Objective C, éstos fueron incluidos.

¿Qué son los bloques?

Los bloques en Objective C y Smalltalk  son «trozos» de código que se pueden guardar en variables, pasar como argumentos, devolver como resultado de un mensaje y ejecutar posteriormente. Es decir, son en el fondo funciones de primer nivel (como en Lisp o cualquier lenguaje funcional), con una sintaxis algo distinta que recuerda los punteros a funciones de C.

Sin embargo, si sólo se tratase de eso, alguien podría decir que los bloques en Objective C son redundantes, ya que con punteros a funciones, se puede hacer todo eso en C de toda la vida, aunque resulte más farragoso y proclive a errores. Los bloques tiene algo más: capturan de forma automática el entorno léxico en el que han sido creados.

Esto quiere decir que si en un método definimos una variable cualquiera (digamos int i = 42) y luego definimos un bloque, dicho bloque podrá hacer referencia a dicha variable. Si pasamos ese bloque a otro objeto, seguirá pudiendo hacer referencia a dicha variable y su valor original. Más adelante veremos eso con más detalle, y su utilidad.

En resumidas cuentas, un bloque es una «closure«.

Uso de los Bloques

En Smalltalk los bloques se utilizan de mil y una formas, incluso para crear expresiones condicionales. Por ejemplo, en Smalltalk, los booleanos no son un tipo «especial». Son objetos, instancias de las clases True o False (ambas descendientes de la clase Boolean). Las expresiones condicionales se obtienen enviando mensajes como:

ifTrue: TrueBlock ifFalse: FalseBlock

En el caso de mandarlo a una instancia de False, solamente ejecutará el FalseBlock.

En Objective C no es una característica fundamental del lenguaje, sino un añadido posterior y su uso no es tan extendido. Aun a si, los bloques son extremadamente útiles para:

  1. Enumeración de colecciones
  2. Ordenación (el bloque se proporciona como el método de comparación)
  3. Notificación (cuando tal cosa ocurra, ejecuta este bloque)
  4. Gestores de error (si ocurre un error mientras haces esto, ejecuta este bloque)
  5. Gestores de finalización (cuando termines de hacer esto, ejecuta este bloque)
  6. Animación de vistas
  7. Por último, tal vez la aplicación más importante en Cocoa: multitarea mediante «Grand Dispatch Central» (GCD).

Sintaxis de bloques en Objective C & Cocoa

La sintaxis que propuso Apple para añadir bloques a C se vio condicionada por la necesidad de mantener compatibilidad con C++. Por lo tanto, se utilizó el acento circunflejo ^ como indicador de inicio de un bloque.

Al parecer era el único operador unario disponible que no podía ser sobrecargado por C++. Ventajas de tener a un tonto en la familia y tener que sentarlo a la mesa…

Declaración

Normalmente se usa un typedef:

typedef double (^unary_block_t) (double op);

Aquí declaramos un bloque llamado unary_block_t que acepta un argumento llamado op de tipo double y que devuelve un double.

Definición

Aquí es donde escribimos el código que ejecutará el bloque. No hace falta especificar el tipo del valor que devuelve, ya que el compilador lo infiere.

unary_block_t square = ^(double op){
     return operand * 2.0;
}; // ¡OJO! Hay que cerrar el bloque con punto y coma.

Para el caso muy común en que no hay ni argumentos ni retorno, la sintaxis es bastante abreviada :

^{
     //blá;
     //blá;
};

Se llaman como una función cualquiera:

square(42);

Captura del entorno léxico

Las variables locales definidas en el mismo método (y antes que él), son capturadas por el bloque, pero son de sólo lectura dentro del mismo.

Básicamente se hace una copia constante de aquellas variables definidas previamente.

BOOL stoppedEarly = NO;
    int i = 42;
    NSDictionary *dict = [NSDictionarydictionaryWithContentsOfURL:
                          [NSURLURLWithString:@"file:///Users/fernando/Junkyard/Vocabulous/vocabwords.txt"]];
    assert(dict);
    [dict enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
        static int j = 0;
        NSLog(@"Value for key %@ is %@", key, obj);
        j += i;
        NSLog(@"%d", j);
        if([@"ENOUGH"isEqualToString:key]){
            *stop = YES;
            //stoppedEarly = YES; // ¡imposible!
        }
    }];

Variables de instancia y punteros

Las variables de instancia, al ser punteros, sí que son modificables: lo que es constante es el puntero. ¡El puntero es constante dentro del bloque, no los datos apuntados por él!

Es muy importante saber que cuando usamos un puntero a un objeto dentro de un bloque, dicho objeto automágicamente recibe un mensaje retain. Será liberado (release) cuando el bloque lo sea a su vez.

Métodos que aceptan un bloque

A menudo no se utiliza un typedef. La sintaxis es exactamente la misma a la del typedef, sólo que se omite la palabra typedef y el nombre del typedef:

-(void) enumerateKeysAndObjectsUsingBlock:(void (^) (id key,id obj, BOOL *stop);

Los bloques son objetos limitados

Los bloques en Cocoa son objetos, aunque algo limitados. Puedes enviar la siguiente lista de mensajes a un bloque:

  1. copy
  2. retain
  3. release
  4. autorelease

Los bloques pueden devolver otros objetos y otros bloques

Ejemplo de bloque que devuelve un objeto:

typedef NSString* (^stringyfier_t)(int x);
    stringyfier_t str = ^(int x) {
        return [NSStringstringWithFormat:@"%d", x];
    };

Podemos por lo tanto implementar bloques que devuelven otros bloques. Esto nos permite implementar cosas como la función adder que menciona Paul Graham en «ANSI Common Lisp«.

La función adder es una ejemplo de función paramétrica que devuelve una versión especializada de sí misma: adder(42) devuelve otra función que devuelve la suma de 42 y el argumento que reciba:

typedef int (^unary_operation_t)(int x);
    typedef unary_operation_t(^adder_t)(int n);

    adder_t adder = ^(int n){
        unary_operation_t fn = ^(int x){
            return n + x;
        };
        return fn;
    };

    unary_operation_t one = adder(1);
    NSLog(@"The %d nights", one(1000));

También se podrían implementar los «streams» mencionados en «Structure and Interpretation of Computer Programs» (SICP) para representar secuencias infinitas de datos.

Fernando Rodríguez

Sígueme en twitter.
Cursos de programación y tecnología