instancetype en Objective C: constructores y métodos factory más seguros

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

Inicializadores en Objective C: algunos métodos son más iguales que otros

Aun recuerdo la extrañeza que me produjo cuando descubrí que los inicializadores en Objective C no devolvían un puntero a la instancia en cuestión, sino un tal de id (un puntero genérico a cualquier objeto). Tardé un poco en percatarme (el libro no o explicaba) que esto era para facilitar la herencia.

Sin embargo, este tipo de valor de retorno, no es lo único especial en los inicializadores. El compilador claramente trata de una forma distinta a aquellos métodos que empiezan por init.

Para empezar, el asignar a self es algo que sólo se nos permite en estos métodos. Por eso siempre insisto en mis cursos de programación iOS que los alumnos se acostumbre a nombrar métodos, clases y variables en Inglés, dado que uno de los factores que usa el compilador para determinar si un método es un inicializador es el nombre (además del tipo de retorno).

He visto muchos alumnos que se meten en líos totalmente innecesarios por nombrar sus inicializadores crearConLoQueSea: en vez de initWithWhatever:.

Los inicializadores  de Objective C saben más de lo crees

La potestad para asignar a self no es la única muestra de trato de favor que reciben los inicializadores. Aunque especifiquemos que nuestro método devuelve id (cualquier objeto), está claro que el compilador no nos hace caso y considera que lo que vamos a devolver es una instancia de la clase que recibe el mensaje.

Veámoslo con un ejemplo, en el que tenemos dos clases:

Class-Hierarchy_instancetype
ClassHierarchy
#import <Foundation/Foundation.h>

@interface AGTStarWarsCharacter : NSObject

+(id) starWarsCharacter;

@end

#import "AGTStarWarsCharacter.h"

@implementation AGTStarWarsCharacter

+(id) starWarsCharacter{
    return [[self alloc] init];
}
@end
#import "AGTStarWarsCharacter.h"

@interface AGTWookie : AGTStarWarsCharacter

+(id) wookie;

-(void)fullBodyWax;

@end

#import "AGTWookie.h"

@implementation AGTWookie
+(id) wookie{
    return [[self alloc] init];
}

-(void)fullBodyWax{
    NSLog(@"AUGUUUGUUUGUUUGRRRGHHHHH!!!!");
}

@end

Si ni AGTStarWars ni AGTWookie implementan el método init y le enviamos dicho mensaje a la clase AGTWookie, el método que se ejecutará será el de NSObject. No obstante, init (aun estando definido en NSObject) NO nos devolverá un NSObject, sino un AGTWookie. Claramente aquí está pasando algo raro, y el compilador está teniendo acceso a más información de la que le estamos dando.

Para que quede claro que el compilador tiene más información de la que le hemos dado, veamos lo que ocurre cuando creamos un objeto de tipo AGTStarWars usando alloc e init, y a objeto resultante, le enviamos el mensaje fullBodyWax (que está definido en la subclase AGTWookie y que AGTStarWarsCharacter NO entiende).

En principio, el compilador no debería de saber qué mensajes entiende el id devuelto por init y no debería dar ningún error de compilación. El error sería en tiempo de ejecución, cuando el objeto no reconozca el mensaje fullBodyWax. Sin embargo, no es así: el compilador lo detecta de inmediato y nos da un error al compilar.

instancetype
Cuando se crea con init, el compilador detecta que el objeto no entiende el mensaje fullBodyWax

Es decir, aunque le decimos que vamos a devolver id, él sabe perfectamente qué tipo de objeto vamos a devolver.

Sin embargo, cuando creo el objeto usando un constructor de conveniencia llamado wookie,  elcompilador acepta sin más que vamos a devolver «algún tipo de objeto» y por lo tanto no tiene como averiguar qué mensajes éste entiende o no.

No nos da un error en tiempo de compilación, sino que casca vilmente en tiempo de ejecución.

instancetype en Objective C: algo más de información para el compilador

¿No estaría bien que otros métodos, que también crean instancias, fuesen igual de «listos» que los inicializadores? Lograrlo es facilísimo, usando una novedad del compilador (ojo, que estas modificaciones no afectan al lenguaje, sino al compilador): el tipo instancetype.

instancetype le indica al compilador que no vamos a devolver cualquier objeto, sino una instancia de la clase que recibe el mensaje. Es decir, cualquier método que devuelva instancetype será igual de «listo» que los inits.

Si cambiamos la definición de wookie para que devuelva instancetype en vez de id, el compilador pillará el error ante de llegar a tiempo de ejecución.

#import <Foundation/Foundation.h>

@interface AGTStarWarsCharacter : NSObject

+(instancetype) starWarsCharacter;

@end

@implementation AGTStarWarsCharacter

+(instancetype) starWarsCharacter{
    return [[self alloc] init];
}
@end
#import "AGTStarWarsCharacter.h"

@interface AGTWookie : AGTStarWarsCharacter

+(instancetype) wookie;

-(void)fullBodyWax;

@end

@implementation AGTWookie
+(instancetype) wookie{
    return [[self alloc] init];
}

-(void)fullBodyWax{
    NSLog(@"AUGUUUGUUUGUUUGRRRGHHHHH!!!!");
}

@end
instancetype
Ahora pilla el error en ambos casos: wookie pasa a ser tratado como un inicializador.

Cómo usar instancetype

Es recomendable que tus métodos «factory» (crean instancias) devuelvan siempre instancetype para tener algo más de seguridad frente al envío de mensajes desconocidos. En los inicializadores no es necesario, ya que el compilador lo hace por nosotros.

¿Es una buena idea instancetype?

Aunque Objective C es un magnífico ejemplo de la escuela de pensamiento contraria (lenguajes dinámicos donde el compilador sabe cual es su sitio y no te toca las pelotas puesto que parte del principio que sabes lo que está haciendo), algunos de los nuevos añadidos recientes parecen ir en dirección contraria.

Uno de ellos es precisamente instancetype. Aunque puede ser de ayuda, más que nada para que el autocompletar sea más preciso, puede ser usado para el Mal.

Un ejemplo horrendo está en esta implementación de colecciones genéricas «seguras»: no me toque UD los corchetes, que las colecciones en Cocoa ya son genéricas de narices y los adultos no necesitamos rueditas extra en la bicicleta.

De momento lo usaré en mis métodos «factory», pero me mantenderé alerta a cualquier nueva herejía…