NSInvocation para torpes

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

NSInvocation

En entornos orientados a objetos muy dinámicos, como es el caso de Cocoa & Objective C o Smalltalk, los mensajes entre objetos están siendo continuamente enviados, redireccionados, guardados para uso posterior, reutilizados y distribuidos a otros objetos. Nada de esto es posible en entornos más estáticos, como pueda ser C++.

La pieza fundamental que permite toda esta flexibilidad es NSInvocation, clase que representa el procedimiento de «enviar un mensaje a alguien».

Qué es NSInvocation

Si como un servidor, ya peinas canas, seguramente recuerdas el antiguo procedimiento para enviar un mensaje a alguien:

  1. Coges un objeto llamado sobre.
  2. Guardas tu mensaje dentro de él.
  3. Añades el destinatario
  4. Lo depositas en un objeto metálico situado en la calle y llamado «buzón de correos».
Pues bien, NSInvocation, no es más que la simulación  Cocoa de dicho sobre, con su mensaje dentro y el destinatario.

Selector + Method Signature = Mensaje

Para tener la especificación completa de un mensaje necesitamos dos cosas:
  1. El «selector»
  2. La «message signature»
El selector es el «nombre» del mensaje, como pueda ser rangeOfString: performSelector: o init. Se obtiene mediante la directiva Objective C @selector y NO tiene ninguna información de tipo. Es decir, no sabe lo que devuelve el mensaje ni qué tipo de parámetros acepta (si acepta alguno).
Está representado en Objective C por el tipo SEL.
La method signature es lo que nos falta. Es decir, contiene el tipo de valor de retorno así como el de los parámetros que acepta.
En Cocoa, está representada por la clase NSMethodSignature y para crear una, se le envía el mensaje methodSignatureForSelector: al objeto en cuestión.
¡OJO! No podía ser una directiva como @selector, por que mientras que varios objetos pueden tener mensajes con el mismo nombre «selector», la method signature depende de quien responde al mensaje.
Es decir, mientras que las clases A y B pueden tener mensajes con el mismo selector (foo:), sus method signatures pueden ser distintas:
  1. A acepta un id como parámetro y devuelve un puntero a A
  2. B acepta una NSString y devuelve un puntero a B.

Crear y configurar una instancia de NSInvocation

El procedimiento sería el siguiente:
  1. Crear un NSMethodSignature tal como se describe en el párrafo anterior
  2. Crear una instancia de NSInvocation pasándole la NSMethodSignature (primera parte del mensaje).
  3. Indicarle al NSInvocation qué selector habrá que usar (segunda parte y última parte del mensaje: ya tiene el mensaje completo).
  4. Indicar a NSInvocation el destinatario del mensaje.
  5. Indicar a NSInvocation los parámetros del mensaje, uno a uno. Ten en cuenta que todo mensaje en Cocoa tiene dos argumentos ocultos (self y _cmd), así que los tuyo empiezan en el índice 2. OJO, que los parámetros se pasan por referencia.
¡Nuestro «sobre» NSInvocation está listo para ser puesto en el buzón.

Enviar el mensaje representado por NSInvocation

Basta con enviarle el mensaje invoke.

Uso de NSInvocation

Ahora ya podemos guardar nuestra NSInvocation en un array, enviársela a otro objeto, guardarla para un uso posterior, cambiar el destinatario y enviar ese mismo mensaje a otro objeto, etc…
En Cocoa, se usa especialmente dos casos:
  1. Implementar el «deshacer» en documentos. Cuando se va a hacer un cambio a un documento, se crea una NSInvocation con el efecto contrario. Si hay que deshacer, simplemente se invoca a esa NSInvocation.
  2. Redireccionamiento de mensajes o «message forwarding«. Cuando un objeto recibe un mensaje que no entiende y ya ha recurrido a sus superclases y ninguna de ellas tampoco lo entiende, queda un último recurso: redireccionarlo a alguien que pueda entenderlo. Para ello se «empaqueta» el mensaje en un NSInvocation y se envía a sí mismo como el parámetro del mensaje forwardInvocation: Tu puedes sobrescribir dicho método y especificar a quién mandar el mensaje problemático. Si esto tampoco funciona, se genera una excepción informando que  el objeto no responde al mensaje problemático.

Guardar una NSInvocation para uso posterior

Si vas a guardar una NSInvocation para usarla más adelante, ya sea en un NSTimer o pasado cierto tiempo, ten en cuenta que NSInvocation NO retiene sus parámetros, así que ¡asegúrate de mandarle el mensaje retainArguments!

Ejemplo de NSInvocation

    // Destinatario del mensaje y argumentos del mismo
    NSString *planeta = @"http://cocoaosx.com/2010/11/30/planeta-cocoa/";
    NSString *bloque = @"http://cocoaosx.com/2011/06/22/como-retrasar-la-ejecucion-de-un-bloque-en-objective-c/";
    NSString *res;
    int option = NSCaseInsensitiveSearch;

    // NSInvocation: el "sobre" en el que meteremos el mensaje
    NSInvocation *env = [NSInvocation invocationWithMethodSignature:
                         [planeta methodSignatureForSelector:
                          @selector(commonPrefixWithString:options:)]];
    [env setTarget:planeta];
    [env setSelector:@selector(commonPrefixWithString:options:)];

    // Los argumentos se pasan por referencia
    [env setArgument:&bloque atIndex:2];
    [env setArgument:&option atIndex:3];

    // Como pienso guardar esta instancia de NSInvocation
    // para usarla más tarde, tengo que retener los argumentos
    // ya que NSinvocation no lo hace por defecto.
    [env retainArguments];

    // Enviamos el sobre.
    [env invoke];

    // Recuperamos el valor de retorno. OJO que se hace por referencia.
    // Como el método empieza por "get" significa que somos reponsables
    // por liberarlo (release). Ver las 3 reglas de oro de la
    // gestión de memoria en Objective C:
    // http://cocoaosx.com/2010/11/20/las-3-reglas-de-oro-de-la-gestion-de-memoria-en-cocoa/    [env getReturnValue:&res];
    NSLog(@"%@", res);
    [res release];

Conclusiones

Aunque el ejemplo en sí no tiene demasiado sentido, sirve como receta de cómo crear y usar una NSInvocation. En otros artículos veremos ejemplos realistas de su uso. Especialmente en el caso de «message forwarding», con el que se pueden hacer verdaderas virguerías (como «Higher Order Messaging», DSLs y demás) así como cagadas portentosas.

Fernando Rodríguez

Sígueme en twitter.
Cursos de desarrollo iPhone