objective c - ¿Sería beneficioso comenzar a usar instancetype en lugar de id?
objective-c (4)
Clang agrega un tipo de palabra clave que, por lo que puedo ver, reemplaza id
como un tipo de retorno en -alloc
e init
.
¿Hay algún beneficio en usar instancetype
lugar de id
?
Definitivamente hay un beneficio. Cuando usas ''id'', básicamente no obtienes ninguna verificación de tipo. Con instancetype, el compilador y el IDE saben qué tipo de cosas se devuelven, y pueden verificar su código mejor y autocompletar mejor.
Solo úselo donde tenga sentido, por supuesto (es decir, un método que devuelve una instancia de esa clase); ID sigue siendo útil.
Las respuestas anteriores son más que suficientes para explicar esta pregunta. Solo me gustaría agregar un ejemplo para que los lectores lo entiendan en términos de codificación.
Clase A
@interface ClassA : NSObject
- (id)methodA;
- (instancetype)methodB;
@end
Clase B
@interface ClassB : NSObject
- (id)methodX;
@end
TestViewController.m
#import "ClassA.h"
#import "ClassB.h"
- (void)viewDidLoad {
[[[[ClassA alloc] init] methodA] methodX]; //This will NOT generate a compiler warning or error because the return type for methodA is id. Eventually this will generate exception at runtime
[[[[ClassA alloc] init] methodB] methodX]; //This will generate a compiler error saying "No visible @interface ClassA declares selector methodX" because the methodB returns instanceType i.e. the type of the receiver
}
Sí, hay beneficios en el uso de instancetype
en todos los casos donde se aplique. Explicaré con más detalle, pero permítame comenzar con esta declaración en negrita: use instancetype
cuando sea apropiado, que es cuando una clase devuelve una instancia de esa misma clase.
De hecho, esto es lo que Apple ahora dice sobre el tema:
En su código, reemplace las ocurrencias de
id
como un valor de retorno con un tipo deinstancetype
cuando sea apropiado. Este suele ser el caso de los métodosinit
y los métodos de clase de fábrica. Aunque el compilador convierte automáticamente los métodos que comienzan con "alloc", "init" o "new" y tienen un tipo deid
de retorno para devolver el tipo deinstancetype
, no convierte otros métodos. La convención de Objective-C es escribir explícitamente el tipo deinstancetype
para todos los métodos.
- Énfasis mío. Fuente: Adoptando Modern Objective-C.
Con eso fuera del camino, sigamos adelante y expliquemos por qué es una buena idea.
Primero, algunas definiciones:
@interface Foo:NSObject
- (id)initWithBar:(NSInteger)bar; // initializer
+ (id)fooWithBar:(NSInteger)bar; // class factory
@end
Para una fábrica de clases, siempre debe usar instancetype
. El compilador no convierte automáticamente id
a instancetype
. Esa id
es un objeto genérico. Pero si lo convierte en un tipo de instancetype
el compilador sabe qué tipo de objeto devuelve el método.
Esto no es un problema académico. Por ejemplo, [[NSFileHandle fileHandleWithStandardOutput] writeData:formattedData]
generará un error en Mac OS X ( solo ) Múltiples métodos llamados ''writeData:'' encontrados con resultados, tipos de parámetros o atributos que no coinciden . La razón es que tanto NSFileHandle como NSURLHandle proporcionan un writeData:
Dado que [NSFileHandle fileHandleWithStandardOutput]
devuelve un id
, el compilador no está seguro de a qué clase writeData:
se está llamando.
Necesitas solucionar esto, usando cualquiera de los dos:
[(NSFileHandle *)[NSFileHandle fileHandleWithStandardOutput] writeData:formattedData];
o:
NSFileHandle *fileHandle = [NSFileHandle fileHandleWithStandardOutput];
[fileHandle writeData:formattedData];
Por supuesto, la mejor solución es declarar fileHandleWithStandardOutput
como devolver un tipo de instancetype
. Entonces el reparto o asignación no es necesario.
(Tenga en cuenta que en iOS, este ejemplo no producirá un error, ya que solo NSFileHandle
proporciona writeData:
existen otros ejemplos, como la length
, que devuelve un CGFloat
de UILayoutSupport
pero un NSUInteger
de NSString
).
Nota : desde que escribí esto, los encabezados de macOS se han modificado para devolver un NSFileHandle
lugar de un id
.
Para los inicializadores, es más complicado. Cuando escribes esto:
- (id)initWithBar:(NSInteger)bar
... el compilador fingirá que escribiste esto en su lugar:
- (instancetype)initWithBar:(NSInteger)bar
Esto fue necesario para ARC. Esto se describe en los tipos de resultados relacionados con las Extensiones de lenguaje Clang. Esta es la razón por la que la gente le dirá que no es necesario usar un tipo de instancetype
, aunque yo sostengo que debería hacerlo. El resto de esta respuesta trata con esto.
Hay tres ventajas:
- Explícito. Su código está haciendo lo que dice, en lugar de otra cosa.
- Modelo. Estás construyendo buenos hábitos para los tiempos que sí importan, los cuales sí existen.
- Consistencia. Ha establecido cierta coherencia en su código, lo que lo hace más legible.
Explícito
Es cierto que no hay ningún beneficio técnico para devolver un tipo de instancetype
desde un init
. Pero esto se debe a que el compilador convierte automáticamente el id
al tipo de instancetype
. Usted está confiando en esta peculiaridad; mientras escribe que el init
devuelve un id
, el compilador lo interpreta como si devolviera un tipo de instancetype
.
Estos son equivalentes al compilador:
- (id)initWithBar:(NSInteger)bar;
- (instancetype)initWithBar:(NSInteger)bar;
Estos no son equivalentes a tus ojos. En el mejor de los casos, aprenderá a ignorar la diferencia y hojearla. Esto no es algo que debas aprender a ignorar.
Modelo
Si bien no hay diferencia con init
y otros métodos, hay una diferencia en cuanto se define una fábrica de clases.
Estos dos no son equivalentes:
+ (id)fooWithBar:(NSInteger)bar;
+ (instancetype)fooWithBar:(NSInteger)bar;
Quieres la segunda forma. Si está acostumbrado a escribir instancetype
como el tipo de retorno de un constructor, siempre lo hará correctamente.
Consistencia
Finalmente, imagine que si lo pone todo junto: desea una función de init
y también una fábrica de clases.
Si usas id
para init
, terminas con un código como este:
- (id)initWithBar:(NSInteger)bar;
+ (instancetype)fooWithBar:(NSInteger)bar;
Pero si usas instancetype
, obtienes esto:
- (instancetype)initWithBar:(NSInteger)bar;
+ (instancetype)fooWithBar:(NSInteger)bar;
Es más consistente y más legible. Ellos devuelven lo mismo, y ahora eso es obvio.
Conclusión
A menos que intencionalmente escriba código para compiladores antiguos, debe usar instancetype
cuando sea apropiado.
Debes dudar antes de escribir un mensaje que devuelva id
. Pregúntate a ti mismo: ¿Es esto volver una instancia de esta clase? Si es así, es un tipo de instancetype
.
Ciertamente hay casos en los que necesitas devolver la id
, pero probablemente usarás el tipo de instancetype
mucha más frecuencia.
También puede obtener detalles en el inicializador designado
**
INSTANCIA
** Esta palabra clave solo se puede utilizar para el tipo de retorno, que coincida con el tipo de receptor de retorno. Método de inicio siempre declarado para devolver el tipo de instancia. ¿Por qué no hacer que el tipo de devolución sea Party for party, por ejemplo? Eso causaría un problema si la clase Party hubiera sido subclasificada. La subclase heredaría todos los métodos de Party, incluido el inicializador y su tipo de retorno. Si se enviara una instancia de la subclase a este mensaje de inicialización, ¿se devolvería? No es un puntero a una instancia de Party, sino un puntero a una instancia de subclase. Podría pensar que no hay problema, anularé el inicializador en la subclase para cambiar el tipo de retorno. Pero en Objective-C, no puede tener dos métodos con el mismo selector y diferentes tipos de retorno (o argumentos). Al especificar que un método de inicialización devuelve "una instancia del objeto receptor", nunca tendría que preocuparse por lo que sucede en esta situación. **
CARNÉ DE IDENTIDAD
** Antes de que se haya introducido el tipo de instancia en Objective-C, los inicializadores devuelven id (ojo-dee). Este tipo se define como "un puntero a cualquier objeto". (id es muy parecido a void * en C). A partir de este escrito, las plantillas de clase XCode todavía usan id como el tipo de retorno de los inicializadores agregados en el código repetitivo. A diferencia de instancetype, id se puede usar como algo más que un tipo de retorno. Puede declarar variables o parámetros de método de tipo id cuando no esté seguro de a qué tipo de objeto terminará apuntando la variable. Puede usar id cuando use la enumeración rápida para iterar sobre una matriz de tipos de objetos múltiples o desconocidos. Tenga en cuenta que debido a que la identificación no está definida como "un puntero a cualquier objeto", no incluye un * al declarar una variable o un parámetro de objeto de este tipo.