Serait-il avantageux de commencer à utiliser instancetype au lieu de id?

226

Clang ajoute un mot clé instancetypequi, autant que je sache, remplace idcomme type de retour dans -allocet init.

Y a-t-il un avantage à utiliser instancetypeau lieu de id?

griotspeak
la source
10
Non .. pas pour alloc et init, car ils fonctionnent déjà comme ça. Le point d'instancetype est pour que vous puissiez donner aux méthodes personnalisées alloc / init un comportement similaire.
hooleyhoop
@hooleyhoop ils ne fonctionnent pas comme ça. Ils retournent id. id est «un objet Obj-C». C'est tout. Le compilateur ne sait rien de la valeur de retour à part cela.
griotspeak
5
ils fonctionnent comme ça, le contrat et la vérification de type sont déjà en cours pour init, seuls les custructors en ont besoin. nshipster.com/instancetype
kocodude
Les choses ont bien avancé, oui. Les types de résultats associés sont relativement nouveaux et d'autres changements signifient que ce sera bientôt un problème beaucoup plus clair.
griotspeak
1
Je voudrais juste dire que maintenant sur iOS 8, beaucoup de méthodes qui idétaient utilisées pour revenir ont été remplacées instancetype, même à initpartir de NSObject. Si vous voulez rendre votre code compatible avec swift, vous devez utiliserinstancetype
Hola Soy Edu Feliz Navidad

Réponses:

192

Il y a certainement un avantage. Lorsque vous utilisez 'id', vous n'obtenez pratiquement aucune vérification de type. Avec instancetype, le compilateur et l'EDI savent quel type de chose est retourné et peuvent mieux vérifier votre code et la saisie semi-automatique.

Utilisez-le uniquement là où cela a du sens bien sûr (c'est-à-dire une méthode qui renvoie une instance de cette classe); id est toujours utile.

Catfish_Man
la source
8
alloc, initEtc. sont automatiquement promu instancetypepar le compilateur. Cela ne veut pas dire qu'il n'y a aucun avantage; il y en a, mais ce n'est pas ça.
Steven Fisher
10
il est surtout utile pour les constructeurs de commodités
Catfish_Man
5
Il a été introduit avec (et pour aider à soutenir) ARC, avec la sortie de Lion en 2011. Une chose compliquant l'adoption généralisée dans Cocoa est que toutes les méthodes candidates doivent être auditées pour voir si elles font [self alloc] plutôt que [NameOfClass alloc], car il serait très déroutant de faire [SomeSubClassommodityConstructor], avec + commoditéConstructor déclaré pour renvoyer instancetype et ne pas renvoyer une instance de SomeSubClass.
Catfish_Man
2
'id' n'est converti en type d'instance que lorsque le compilateur peut déduire la famille de méthodes. Il ne le fait certainement pas pour les constructeurs de commodité ou d'autres méthodes de retour d'ID.
Catfish_Man
4
Notez que dans iOS 7, de nombreuses méthodes dans Foundation ont été converties en type d'instance. J'ai personnellement vu cette capture au moins 3 cas de code préexistant incorrect.
Catfish_Man
336

Oui, il y a des avantages à utiliser instancetypedans tous les cas où cela s'applique. J'expliquerai plus en détail, mais permettez-moi de commencer par cette déclaration en gras: utilisez instancetypechaque fois que cela est approprié, c'est-à-dire chaque fois qu'une classe renvoie une instance de cette même classe.

En fait, voici ce qu'Apple dit maintenant sur le sujet:

Dans votre code, remplacez les occurrences de idcomme valeur de retour par le instancetypecas échéant. C'est généralement le cas pour les initméthodes et les méthodes de fabrique de classe. Même si le compilateur convertit automatiquement les méthodes qui commencent par «alloc», «init» ou «new» et ont un type de idretour à renvoyer instancetype, il ne convertit pas les autres méthodes. La convention Objective-C consiste à écrire instancetypeexplicitement pour toutes les méthodes.

Avec cela à l'écart, passons à autre chose et expliquons pourquoi c'est une bonne idée.

Tout d'abord, quelques définitions:

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

Pour une usine de classe, vous devez toujours utiliser instancetype. Le compilateur ne se convertit pas automatiquement iden instancetype. C'est idun objet générique. Mais si vous en faites un, instancetypele compilateur sait quel type d'objet la méthode retourne.

Ce n'est pas un problème académique. Par exemple, [[NSFileHandle fileHandleWithStandardOutput] writeData:formattedData]générera une erreur sur Mac OS X ( uniquement ) Plusieurs méthodes nommées «writeData:» trouvées avec un résultat, un type de paramètre ou des attributs incompatibles . La raison en est que NSFileHandle et NSURLHandle fournissent a writeData:. Depuis [NSFileHandle fileHandleWithStandardOutput]retourne un id, le compilateur n'est pas certain de quelle classe writeData:est appelée.

Vous devez contourner cela en utilisant soit:

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

ou:

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

Bien sûr, la meilleure solution est de déclarer fileHandleWithStandardOutputcomme retournant un instancetype. Ensuite, le casting ou l'affectation n'est pas nécessaire.

(Notez que sur iOS, cet exemple ne produira pas d'erreur car il n'en NSFileHandlefournit qu'un writeData:. D'autres exemples existent, tels que length, qui renvoie un CGFloatde UILayoutSupportmais un NSUIntegerde NSString.)

Remarque : depuis que j'ai écrit ceci, les en-têtes macOS ont été modifiés pour renvoyer un NSFileHandleau lieu d'un id.

Pour les initialiseurs, c'est plus compliqué. Lorsque vous tapez ceci:

- (id)initWithBar:(NSInteger)bar

… Le compilateur prétendra que vous avez tapé ceci à la place:

- (instancetype)initWithBar:(NSInteger)bar

C'était nécessaire pour l'ARC. Ceci est décrit dans les types de résultats Clang Language Extensions Related . C'est pourquoi les gens vous diront qu'il n'est pas nécessaire d'utiliser instancetype, bien que je soutienne que vous devriez. Le reste de cette réponse traite de cela.

Il y a trois avantages:

  1. Explicite. Votre code fait ce qu'il dit, plutôt que quelque chose d'autre.
  2. Modèle. Vous construisez de bonnes habitudes pour les moments où cela compte, qui existent.
  3. Cohérence. Vous avez établi une certaine cohérence à votre code, ce qui le rend plus lisible.

Explicite

C'est vrai qu'il n'y a aucun avantage technique à revenir instancetyped'un init. Mais c'est parce que le compilateur convertit automatiquement le fichier iden instancetype. Vous comptez sur cette bizarrerie; pendant que vous écrivez que le initrenvoie un id, le compilateur l'interprète comme s'il renvoyait un instancetype.

Ceux-ci sont équivalents au compilateur:

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

Ce ne sont pas équivalents à vos yeux. Au mieux, vous apprendrez à ignorer la différence et à la survoler. Ce n'est pas quelque chose que vous devez apprendre à ignorer.

Modèle

Bien qu'il n'y ait pas de différence avec les initautres méthodes, il y a une différence dès que vous définissez une fabrique de classe.

Ces deux ne sont pas équivalents:

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

Vous voulez le deuxième formulaire. Si vous avez l'habitude de taper instancetypecomme type de retour d'un constructeur, vous aurez toujours raison.

Cohérence

Enfin, imaginez si vous mettez tout cela ensemble: vous voulez une initfonction et aussi une fabrique de classe.

Si vous utilisez idpour init, vous vous retrouvez avec un code comme celui-ci:

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

Mais si vous utilisez instancetype, vous obtenez ceci:

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

C'est plus cohérent et plus lisible. Ils retournent la même chose, et maintenant c'est évident.

Conclusion

À moins que vous n'écriviez intentionnellement du code pour d'anciens compilateurs, vous devez l'utiliser instancetypelorsque cela est approprié.

Vous devriez hésiter avant d'écrire un message qui revient id. Posez-vous la question: cela renvoie-t-il une instance de cette classe? Si oui, c'est un instancetype.

Il y a certainement des cas où vous devez revenir id, mais vous en utiliserez probablement instancetypebeaucoup plus fréquemment.

Steven Fisher
la source
4
Cela fait un moment que je n'ai pas posé cette question et j'ai depuis longtemps adopté une position comme celle que vous proposez ici. Je laisse tomber ici parce que je pense que, malheureusement, cela sera considéré comme une question de style pour init et les informations présentées dans les réponses précédentes ont répondu à la question assez clairement.
griotspeak
6
Pour être clair: je crois que la réponse de Catfish_Man n'est correcte que dans sa première phrase . La réponse de hooleyhoop est correcte à l' exception de sa première phrase . C'est un enfer d'un dilemme; Je voulais fournir quelque chose qui, au fil du temps, serait considéré comme plus utile et plus correct que les deux. (Avec tout le respect que je dois à ces deux-là; après tout, cela est beaucoup plus évident maintenant qu'il ne l'était lorsque leurs réponses ont été écrites.)
Steven Fisher
1
Avec le temps, je l'espère. Je ne vois aucune raison de ne pas le faire, à moins qu'Apple ne juge important de maintenir la compatibilité des sources avec (par exemple) l'attribution d'un nouveau NSArray à une NSString sans distribution.
Steven Fisher
4
instancetypevs idn'est vraiment pas une décision de style. Les récents changements autour de instancetypenous instancetype-init
montrent
2
Vous avez dit qu'il y avait des raisons d'utiliser id, pouvez-vous élaborer? Les deux seuls auxquels je peux penser sont la brièveté et la compatibilité descendante; considérant que les deux sont au détriment de l'expression de votre intention, je pense que ce sont vraiment de mauvais arguments.
Steven Fisher
10

Les réponses ci-dessus sont plus que suffisantes pour expliquer cette question. Je voudrais juste ajouter un exemple pour que les lecteurs le comprennent en termes de codage.

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
la source
cela ne générera une erreur de compilation que si vous n'utilisez pas #import "ClassB.h". sinon, le compilateur supposera que vous savez ce que vous faites. par exemple, le code suivant se compile, mais se bloque lors de l'exécution: id myNumber = @ (1); id iAssumedThatWasAnArray = myNumber [0]; id iAssumedThatWasADictionary = [myNumber objectForKey: @ "key"];
OlDor
1

Vous pouvez également obtenir des détails dans l' initialiseur désigné

**

INSTANCETYPE

** Ce mot-clé ne peut être utilisé que pour le type de retour, qu'il correspond au type de retour du récepteur. La méthode init a toujours déclaré renvoyer instancetype. Pourquoi ne pas faire du type de retour Party par exemple Party, par exemple? Cela causerait un problème si la classe Party était jamais sous-classée. La sous-classe hériterait de toutes les méthodes de Party, y compris l'initialiseur et son type de retour. Si une instance de la sous-classe recevait ce message d'initialisation, ce serait return? Pas un pointeur vers une instance de Party, mais un pointeur vers une instance de sous-classe. Vous pourriez penser que ce n'est pas un problème, je remplacerai l'initialiseur dans la sous-classe pour changer le type de retour. Mais dans Objective-C, vous ne pouvez pas avoir deux méthodes avec le même sélecteur et différents types de retour (ou arguments). En spécifiant qu'une méthode d'initialisation renvoie "

Identifiant

** Avant l'introduction du type d'instance dans Objective-C, les initialiseurs renvoient id (eye-dee). Ce type est défini comme "un pointeur vers n'importe quel objet". (id ressemble beaucoup à void * en C.) À ce jour, les modèles de classe XCode utilisent toujours id comme type de retour des initialiseurs ajoutés dans le code passe-partout. Contrairement à instancetype, id peut être utilisé comme plus qu'un simple type de retour. Vous pouvez déclarer des variables ou des paramètres de méthode de type id lorsque vous ne savez pas quel type d'objet la variable finira par pointer. Vous pouvez utiliser id lorsque vous utilisez l'énumération rapide pour itérer sur un tableau de types d'objets multiples ou inconnus. Notez que comme id n'est pas défini comme "un pointeur vers n'importe quel objet", vous n'incluez pas de * lorsque vous déclarez une variable ou un paramètre d'objet de ce type.

Nam N. HUYNH
la source