Seria benéfico começar a usar o tipo de instância em vez do id?

226

Clang adiciona uma palavra-chave instancetypeque, até onde posso ver, substitui idcomo tipo de retorno em -alloce init.

Existe um benefício em usar em instancetypevez de id?

griotspeak
fonte
10
Não. Não para alocação e init, pois eles já funcionam assim. O ponto do tipo de instância é para que você possa atribuir / init métodos personalizados como behavoir.
Hooleyhoop 6/03/12
@hooleyhoop eles não funcionam assim. Eles retornam identificação. id é 'um objeto Obj-C'. Isso é tudo. O compilador não sabe nada sobre o valor de retorno além disso.
Griotspeak
5
eles funcionam assim, o contrato e a verificação de tipo já estão acontecendo para o init, apenas para os fabricantes que você precisa. nshipster.com/instancetype
kocodude
As coisas avançaram bastante, sim. Os tipos de resultados relacionados são relativamente novos e outras alterações significam que esse será um problema muito mais claro em breve.
griotspeak
1
Gostaria apenas de comentar que agora no iOS 8 muitos métodos que costumavam retornar idforam substituídos instancetype, mesmo a initpartir de NSObject. Se você deseja tornar seu código compatível com o swift, você deve usarinstancetype
Hola Soy Edu Feliz Navidad

Respostas:

192

Definitivamente, há um benefício. Quando você usa 'id', você não obtém praticamente nenhuma verificação de tipo. Com o instancetype, o compilador e o IDE sabem que tipo de coisa está sendo retornada e podem verificar melhor seu código e preencher automaticamente melhor.

Use-o apenas onde for mais claro (isto é, um método que está retornando uma instância dessa classe); id ainda é útil.

Catfish_Man
fonte
8
alloc, initetc. são automaticamente promovidos instancetypepelo compilador. Isso não quer dizer que não há benefício; existe, mas não é isso.
Steven Fisher
10
é útil para os construtores de conveniência em sua maioria
Catfish_Man
5
Foi introduzido com (e para ajudar a apoiar) o ARC, com o lançamento do Lion em 2011. Uma coisa que complica a adoção generalizada no Cocoa é que todos os métodos candidatos precisam ser auditados para ver se eles se auto-alocam em vez de [NameOfClass alocar], porque seria super confuso fazer [SomeSubClass convenientConstructor], com + convenientConstructor declarado para retornar instancetype, e não devolver uma instância de SomeSubClass.
Catfish_Man
2
'id' só é convertido em tipo de instância quando o compilador pode inferir a família de métodos. Certamente não o faz para construtores de conveniência ou outros métodos de retorno de ID.
Catfish_Man
4
Observe que no iOS 7, muitos métodos no Foundation foram convertidos para o tipo de instância. Eu pessoalmente vi isso capturar pelo menos três casos de código pré-existente incorreto.
Catfish_Man
336

Sim, há benefícios em usar instancetypeem todos os casos em que se aplica. Explicarei com mais detalhes, mas deixe-me começar com esta declaração em negrito: Use instancetypesempre que apropriado, ou seja, sempre que uma classe retornar uma instância dessa mesma classe.

De fato, eis o que a Apple diz agora sobre o assunto:

No seu código, substitua as ocorrências idcomo um valor de retorno por instancetypeonde apropriado. Normalmente, esse é o caso dos initmétodos e métodos de fábrica de classe. Mesmo que o compilador converta automaticamente métodos que começam com "alocar", "init" ou "novo" e têm um tipo de idretorno a ser retornado instancetype, ele não converte outros métodos. A convenção Objective-C é escrever instancetypeexplicitamente para todos os métodos.

Com isso fora do caminho, vamos seguir em frente e explicar por que é uma boa ideia.

Primeiro, algumas definições:

 @interface Foo:NSObject
 - (id)initWithBar:(NSInteger)bar; // initializer
 + (id)fooWithBar:(NSInteger)bar;  // class factory
 @end

Para uma fábrica de classe, você deve sempre usar instancetype. O compilador não converte automaticamente idpara instancetype. Esse idé um objeto genérico. Mas se você o fizer, instancetypeo compilador saberá que tipo de objeto o método retornará.

Este não é um problema acadêmico. Por exemplo, [[NSFileHandle fileHandleWithStandardOutput] writeData:formattedData]gerará um erro no Mac OS X ( apenas ) Vários métodos denominados 'writeData:' encontrados com resultado, tipo de parâmetro ou atributos incompatíveis . O motivo é que NSFileHandle e NSURLHandle fornecem um writeData:. Como [NSFileHandle fileHandleWithStandardOutput]retorna um id, o compilador não tem certeza de qual classe writeData:está sendo chamada.

Você precisa contornar isso, usando:

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

ou:

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

Obviamente, a melhor solução é declarar fileHandleWithStandardOutputcomo retornando um instancetype. Então o elenco ou a tarefa não são necessários.

(Observe que no iOS, este exemplo não produzirá um erro, pois apenas NSFileHandlefornece um writeData:lá. Existem outros exemplos, como length, que retorna um CGFloatde UILayoutSupportmas um NSUIntegerde NSString.)

Nota : Desde que escrevi isso, os cabeçalhos do macOS foram modificados para retornar um em NSFileHandlevez de um id.

Para inicializadores, é mais complicado. Quando você digita isso:

- (id)initWithBar:(NSInteger)bar

… O compilador fingirá que você digitou isso:

- (instancetype)initWithBar:(NSInteger)bar

Isso foi necessário para o ARC. Isso é descrito em Tipos de resultados relacionados ao Clang Language Extensions . É por isso que as pessoas lhe dirão que não é necessário usá-lo instancetype, embora eu afirme que você deveria. O restante desta resposta lida com isso.

Há três vantagens:

  1. Explícito. Seu código está fazendo o que diz, em vez de outra coisa.
  2. Padronizar. Você está construindo bons hábitos para tempos que importam e que existem.
  3. Consistência. Você estabeleceu alguma consistência no seu código, o que o torna mais legível.

Explícito

É verdade que não há benefício técnico em retornar instancetypede um init. Mas isso ocorre porque o compilador converte automaticamente o idpara instancetype. Você está confiando nessa peculiaridade; enquanto você escreve que initretorna um id, o compilador o interpreta como se retornasse um instancetype.

Eles são equivalentes ao compilador:

- (id)initWithBar:(NSInteger)bar;
- (instancetype)initWithBar:(NSInteger)bar;

Estes não são equivalentes aos seus olhos. Na melhor das hipóteses, você aprenderá a ignorar a diferença e examiná-la. Isso não é algo que você deve aprender a ignorar.

padronizar

Enquanto não há nenhuma diferença com inite outros métodos, não é uma diferença assim que você definir uma fábrica de classe.

Estes dois não são equivalentes:

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

Você quer o segundo formulário. Se você está acostumado a digitar instancetypecomo o tipo de retorno de um construtor, sempre acertará.

Consistência

Por fim, imagine se você juntar tudo: deseja uma initfunção e também uma fábrica de classes.

Se você usar idpara init, você acaba com um código como este:

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

Mas se você usar instancetype, você obtém o seguinte:

- (instancetype)initWithBar:(NSInteger)bar;
+ (instancetype)fooWithBar:(NSInteger)bar;

É mais consistente e mais legível. Eles retornam a mesma coisa, e agora isso é óbvio.

Conclusão

A menos que você esteja intencionalmente escrevendo código para compiladores antigos, use-o instancetypequando apropriado.

Você deve hesitar antes de escrever uma mensagem que retorne id. Pergunte a si mesmo: isso está retornando uma instância desta classe? Se sim, é um instancetype.

Certamente há casos em que você precisa retornar id, mas provavelmente usará com instancetypemuito mais frequência.

Steven Fisher
fonte
4
Faz um tempo desde que perguntei isso e há muito tempo adotei uma postura como a que você colocou aqui. Deixei-o aqui porque acho que, infelizmente, isso será considerado uma questão de estilo para o init e as informações apresentadas nas respostas anteriores responderam à pergunta com bastante clareza.
Griotspeak:
6
Para ser claro: acredito que a resposta de Catfish_Man está correta apenas em sua primeira frase . A resposta por hooleyhoop está correta, exceto pela primeira frase . É um dilema infernal; Eu queria fornecer algo que, com o tempo, fosse visto como mais útil e mais correto do que qualquer um. (Com todo o respeito a esses dois, afinal, este é muito mais evidente agora do que era quando as suas respostas foram escritas.)
Steven Fisher
1
Com o tempo, espero que sim. Não consigo pensar em nenhum motivo para não fazê-lo, a menos que a Apple considere importante manter a compatibilidade de origem com (por exemplo) a atribuição de um novo NSArray a um NSString sem elenco.
9133 Steven Fisher
4
instancetypevs idrealmente não é uma decisão de estilo. As recentes mudanças ao redor instancetyperealmente deixam claro que devemos usar instancetypeem lugares como o -initque queremos dizer 'uma instância da minha classe'
griotspeak
2
Você disse que havia razões para usar o id, você pode elaborar? Os únicos dois em que consigo pensar são concisão e compatibilidade com versões anteriores; considerando que ambos estão à custa de expressar sua intenção, acho que esses argumentos são realmente ruins.
9408 Steven Fisher
10

As respostas acima são mais que suficientes para explicar esta pergunta. Gostaria apenas de adicionar um exemplo para os leitores entenderem em termos de codificação.

Classe A

@interface ClassA : NSObject

- (id)methodA;
- (instancetype)methodB;

@end

Classe 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
}
Evol Gate
fonte
isso gerará apenas um erro do compilador se você não usar #import "ClassB.h". caso contrário, o compilador assumirá que você sabe o que está fazendo. por exemplo, o seguinte compila, mas falha em tempo de execução: id myNumber = @ (1); id iAssumedThatWasAnArray = myNumber [0]; id iAssumedThatWasADictionary = [myNumber objectForKey: @ "key"];
precisa saber é
1

Você também pode obter detalhes em The Designated Initializer

**

INSTANCETYPE

** Esta palavra-chave pode ser usada apenas para o tipo de retorno, que corresponde ao tipo de retorno do receptor. O método init sempre declara retornar o tipo de instância. Por que não fazer o tipo de retorno Parte para instância de parte, por exemplo? Isso causaria um problema se a classe Party fosse subclassificada. A subclasse herdaria todos os métodos do Party, incluindo o inicializador e seu tipo de retorno. Se uma instância da subclasse recebesse essa mensagem inicializadora, isso seria retornado? Não é um ponteiro para uma instância do Party, mas um ponteiro para uma instância da subclasse. Você pode pensar que isso não tem problema, substituirei o inicializador na subclasse para alterar o tipo de retorno. Mas no Objective-C, você não pode ter dois métodos com o mesmo seletor e diferentes tipos de retorno (ou argumentos). Especificando que um método de inicialização retorne "

EU IRIA

** Antes que o tipo de instância seja introduzido no Objective-C, os inicializadores retornam o id (eye-dee). Este tipo é definido como "um ponteiro para qualquer objeto". (id é muito parecido com void * em C.) Até o momento, os modelos de classe XCode ainda usam id como o tipo de retorno de inicializadores adicionados no código padrão. Ao contrário do tipo de instância, o id pode ser usado como mais do que apenas um tipo de retorno. Você pode declarar variáveis ​​ou parâmetros de método do tipo id quando não tiver certeza de que tipo de objeto a variável acabará apontando. Você pode usar a identificação ao usar a enumeração rápida para iterar em uma matriz de vários tipos de objetos ou desconhecidos. Observe que, como id é indefinido como "um ponteiro para qualquer objeto", você não inclui um * ao declarar uma variável ou parâmetro de objeto desse tipo.

Nam N. HUYNH
fonte