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?


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.


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.


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
}

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.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.