¿Sería beneficioso comenzar a usar tipo de instancia en lugar de id?

226

Clang agrega una palabra clave instancetypeque, por lo que puedo ver, reemplaza idcomo un tipo de retorno en -allocy init.

¿Hay algún beneficio en usar en instancetypelugar de id?

griotspeak
fuente
10
No ... no para alloc e init, ya que funcionan de esta manera. El punto de tipo de instancia es para que pueda dar métodos personalizados alloc / init como behavoir.
hooleyhoop
@hooleyhoop no funcionan así. Vuelven id. id es 'un objeto Obj-C'. Eso es todo. El compilador no sabe nada sobre el valor de retorno aparte de eso.
griotspeak 01 de
55
funcionan de esta manera, el contrato y la verificación de tipo ya está ocurriendo para init, solo para custructors necesita esto. nshipster.com/instancetype
kocodude
Las cosas han avanzado bastante, sí. Los tipos de resultados relacionados son relativamente nuevos y otros cambios significan que pronto será un tema mucho más claro.
griotspeak
1
Solo me gustaría comentar que ahora en iOS 8 idse han reemplazado muchos métodos que solían regresar instancetype, incluso initdesde NSObject. Si desea que su código sea compatible con Swift, debe usarloinstancetype
Hola Soy Edu Feliz Navidad

Respuestas:

192

Definitivamente hay un beneficio. Cuando usas 'id', básicamente no obtienes ninguna verificación de tipo. Con el tipo de instancia, el compilador y el IDE saben qué tipo de cosas se devuelven, y pueden verificar su código mejor y autocompletar mejor.

Úselo solo donde tenga sentido, por supuesto (es decir, un método que devuelve una instancia de esa clase); Identificación sigue siendo útil.

Catfish_Man
fuente
8
alloc, init, Etc., están promovidos automáticamente instancetypepor el compilador. Eso no quiere decir que no haya beneficio; hay, pero no es esto.
Steven Fisher
10
es útil para los constructores de conveniencia en su mayoría
Catfish_Man 05 de
55
Fue introducido con (y para ayudar a apoyar) ARC, con el lanzamiento de Lion en 2011. Una cosa que complica la adopción generalizada en Cocoa es que todos los métodos candidatos deben ser auditados para ver si funcionan [self alloc] en lugar de [NameOfClass alloc], porque sería muy confuso hacer [SomeSubClass convenienceConstructor], con + convenienceConstructor declarado para devolver el tipo de instancia, y que no devuelva una instancia de SomeSubClass.
Catfish_Man
2
'id' solo se convierte a tipo de instancia cuando el compilador puede inferir la familia de métodos. Ciertamente no lo hace por conveniencia de los constructores u otros métodos de devolución de id.
Catfish_Man
44
Tenga en cuenta que en iOS 7, muchos métodos en Foundation se han convertido a tipo de instancia. Personalmente he visto esta captura de al menos 3 casos de código preexistente incorrecto.
Catfish_Man
336

Sí, hay beneficios de usar instancetypeen todos los casos donde se aplica. Explicaré con más detalle, pero permítanme comenzar con esta declaración en negrita: useinstancetype siempre que sea apropiado, que es cuando una clase devuelve una instancia de esa misma clase.

De hecho, esto es lo que Apple dice ahora sobre el tema:

En su código, reemplace las ocurrencias de idcomo valor de retorno por instancetypedonde corresponda. Este suele ser el caso de los initmétodos y 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 de idretorno de return instancetype, no convierte otros métodos. La convención Objective-C es escribir instancetypeexplícitamente para todos los métodos.

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 clase, siempre debe usar instancetype. El compilador no se convierte automáticamente ida instancetype. Ese ides un objeto genérico. Pero si lo haces uninstancetype compilador, sabe qué tipo de objeto devuelve el método.

Este 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 no coincidentes, tipo de parámetro o atributos . La razón es que tanto NSFileHandle como NSURLHandle proporcionan a writeData:. Como [NSFileHandle fileHandleWithStandardOutput]devuelve un id, el compilador no está seguro de qué clasewriteData: se está llamando.

Necesita solucionar esto, usando:

[(NSFileHandle *)[NSFileHandle fileHandleWithStandardOutput] writeData:formattedData];

o:

NSFileHandle *fileHandle = [NSFileHandle fileHandleWithStandardOutput];
[fileHandle writeData:formattedData];

Por supuesto, la mejor solución es declarar fileHandleWithStandardOutputque devuelve uninstancetype . Entonces el reparto o la tarea no es necesaria.

(Tenga en cuenta que en iOS, este ejemplo no producirá un error, ya que solo NSFileHandleproporciona un writeData:allí. Existen otros ejemplos, como, por ejemplo length, que devuelve un CGFloatde UILayoutSupportpero un NSUIntegerde NSString).

Nota : desde que escribí esto, los encabezados de macOS se han modificado para devolver un en NSFileHandlelugar 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 era necesario para ARC. Esto se describe en Extensiones de lenguaje Clang Tipos de resultados relacionados . Es por eso que la gente le dirá que no es necesario usarlo instancetype, aunque yo afirmo que debería hacerlo. El resto de esta respuesta trata de esto.

Hay tres ventajas:

  1. Explícito. Su código está haciendo lo que dice, en lugar de otra cosa.
  2. Patrón. Estás creando buenos hábitos para los tiempos que importan, que existen.
  3. Consistencia. Ha establecido cierta consistencia en su código, lo que lo hace más legible.

Explícito

Es cierto que no hay ningún beneficio técnico al regresar instancetypede un init. Pero esto se debe a que el compilador convierte automáticamente el ida instancetype. Estás confiando en este capricho; mientras escribe que initdevuelve un id, el compilador lo interpreta como si devolviera uninstancetype .

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 pasarla por alto. Esto no es algo que debas aprender a ignorar.

Patrón

Si bien no hay ninguna diferencia con initotros métodos, no es una diferencia tan pronto como se define un generador de clases.

Estos dos no son equivalentes:

+ (id)fooWithBar:(NSInteger)bar;
+ (instancetype)fooWithBar:(NSInteger)bar;

Quieres la segunda forma. Si está acostumbrado a escribir instancetypecomo el tipo de retorno de un constructor, siempre lo hará bien.

Consistencia

Finalmente, imagina si lo pones todo junto: quieres una initfunción y también una fábrica de clase.

Si usas idpara 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. Vuelven lo mismo, y ahora eso es obvio.

Conclusión

A menos que esté escribiendo código intencionalmente para compiladores antiguos, debe usarlo instancetypecuando sea apropiado.

Debes dudar antes de escribir un mensaje que regrese id. Pregúntese: ¿Esto devuelve una instancia de esta clase? Si es así, es un instancetype.

Ciertamente, hay casos en los que necesita regresar id, pero probablemente lo usará con instancetypemucha más frecuencia.

Steven Fisher
fuente
44
Ha pasado un tiempo desde que pregunté esto y hace mucho tiempo que adopté una postura como la que usted presentó aquí. Lo dejé ir aquí porque creo que, desafortunadamente, esto se considerará una cuestión de estilo para init y la información presentada en las respuestas anteriores respondió a la pregunta con bastante claridad.
griotspeak
66
Para ser claros: creo que la respuesta de Catfish_Man es correcta solo en su primera oración . La respuesta de hooleyhoop es correcta excepto por su primera oración . Es un gran dilema; Quería proporcionar algo que, con el tiempo, se consideraría más útil y más correcto que cualquiera de los dos. (Con el debido respeto a esos dos; después de todo, esto es mucho más obvio ahora que cuando se escribieron sus respuestas.)
Steven Fisher
1
Con el tiempo, eso espero. No se me ocurre ninguna razón para no hacerlo, a menos que Apple sienta que es importante mantener la compatibilidad de origen con (por ejemplo) la asignación de un nuevo NSArray a un NSString sin un elenco.
Steven Fisher
44
instancetypevs idrealmente no es una decisión de estilo. Los cambios recientes instancetyperealmente dejan en claro que debemos usar instancetypeen lugares como -initdonde queremos decir 'una instancia de mi clase'
griotspeak
2
Dijiste que había razones para usar id, ¿puedes explicarlo? Los únicos dos que puedo pensar son brevedad y compatibilidad con versiones anteriores; teniendo en cuenta que ambos son a expensas de expresar su intención, creo que esos son argumentos realmente pobres.
Steven Fisher
10

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
}
Puerta Evol
fuente
esto solo generará un error de compilación si no usa #import "ClassB.h". de lo contrario, el compilador asumirá que sabes lo que estás haciendo. por ejemplo, las siguientes compilaciones, pero fallan en el tiempo de ejecución: id myNumber = @ (1); id iAssumedThatWasAnArray = myNumber [0]; id iAssumedThatWasADictionary = [myNumber objectForKey: @ "key"];
OlDor
1

También puede obtener detalles en The Designated Initializer

** **

TIPO DE INSTANCIA

** Esta palabra clave solo se puede usar para el tipo de retorno, que coincide con el tipo de retorno del receptor. El método init siempre declara devolver el tipo de instancia. ¿Por qué no hacer que el tipo de devolución Party para party, por ejemplo? Eso causaría un problema si la clase Party alguna vez se subclasificara. La subclase heredaría todos los métodos de Party, incluido el inicializador y su tipo de retorno. Si a una instancia de la subclase se le envió este mensaje inicializador, ¿eso sería devuelto? No es un puntero a una instancia de Party, sino un puntero a una instancia de subclase. Puede 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 "

CARNÉ DE IDENTIDAD

** Antes de que se introdujera el tipo de instancia en Objective-C, los inicializadores devuelven id (eye-dee). Este tipo se define como "un puntero a cualquier objeto". (id es muy parecido a void * en C.) Al momento de escribir esto, las plantillas de clase XCode todavía usan id como el tipo de retorno de inicializadores agregados en el código repetitivo. A diferencia del tipo de instancia, id puede usarse como algo más que un tipo de retorno. Puede declarar variables o parámetros de método de identificación de tipo cuando no está seguro de a qué tipo de objeto la variable terminará apuntando. Puede usar id cuando usa 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 ningún objeto", no incluye un * al declarar una variable o parámetro de objeto de este tipo.

Nam N. HUYNH
fuente