Было бы полезно начать использовать instancetype вместо id?

226

Clang добавляет ключевое слово, instancetypeкоторое, насколько я вижу, заменяет idтип возврата в -allocи init.

Есть ли польза от использования instancetypeвместо id?

griotspeak
источник
10
Нет .. не для alloc и init, так как они уже работают так. Смысл instancetype заключается в том, что вы можете задавать пользовательские методы alloc / init, такие как поведениеvoir.
hooleyhoop
@ holeyhoop они так не работают. Они возвращают идентификатор. id - это объект Obj-C. Вот и все. Компилятор ничего не знает о возвращаемом значении, кроме этого.
Griotspeak
5
они работают так: проверка контракта и типа уже выполняется для init, это нужно только для custructor. nshipster.com/instancetype
kocodude
Вещи продвинулись совсем немного, да. Связанные типы результатов являются относительно новыми, а другие изменения означают, что это будет гораздо более ясной проблемой в ближайшее время.
Griotspeak
1
Я просто хотел бы прокомментировать, что теперь в iOS 8 многие методы, которые использовались для возврата id, были заменены instancetype, даже initиз NSObject. Если вы хотите, чтобы ваш код был совместим с Swift, вы должны использоватьinstancetype
Hola Soy Edu Feliz Navidad

Ответы:

192

Там определенно есть выгода. Когда вы используете 'id', вы вообще не получаете проверку типов. С помощью instancetype компилятор и IDE знают, какой тип объектов возвращается, и могут лучше проверять ваш код и лучше выполнять автозаполнение.

Используйте его только тогда, когда это имеет смысл (т. Е. Метод, который возвращает экземпляр этого класса); Идентификатор все еще полезен.

Catfish_Man
источник
8
alloc, initИ т.д. автоматически повышены до instancetypeкомпилятора. Это не значит, что нет никакой пользы; есть, но это не так.
Стивен Фишер
10
в основном это полезно для конструкторов удобства
Catfish_Man
5
Он был представлен с (и для поддержки) ARC, с выпуском Lion в 2011 году. Одна вещь, усложняющая широкое распространение в Какао, заключается в том, что все методы-кандидаты должны быть проверены, чтобы увидеть, выполняют ли они [self alloc], а не [NameOfClass alloc], потому что было бы очень непонятно сделать [SomeSubClass удобствоКонструктор], с + удобствомКонструктор, объявленным как возвращающий тип экземпляра, и не возвращать экземпляр SomeSubClass.
Cat_Man
2
'id' преобразуется в instancetype только тогда, когда компилятор может определить семейство методов. Это, безусловно, не делает это для удобных конструкторов или других методов, возвращающих id.
Catfish_Man
4
Обратите внимание, что в iOS 7 многие методы в Foundation были преобразованы в instancetype. Я лично видел этот улов как минимум в 3 случаях неправильного ранее существующего кода.
Catfish_Man
336

Да, есть преимущества использования instancetypeво всех случаях, когда это применимо. Я объясню более подробно, но позвольте мне начать с этого жирного выражения: используйте, instancetypeкогда это уместно, то есть всякий раз, когда класс возвращает экземпляр того же класса.

Фактически, вот что Apple сейчас говорит по этому вопросу:

В своем коде замените вхождения idв качестве возвращаемого значения на instancetypeсоответствующие. Обычно это относится к initметодам и методам фабрики классов. Хотя компилятор автоматически преобразует методы, которые начинаются с «alloc», «init» или «new» и имеют возвращаемый тип idдля возврата instancetype, он не преобразует другие методы. Соглашение Objective C состоит в том, чтобы писать instancetypeявно для всех методов.

После этого давайте продолжим и объясним, почему это хорошая идея.

Сначала несколько определений:

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

Для фабрики классов вы всегда должны использовать instancetype. Компилятор не конвертируется автоматически idв instancetype. Это idобщий объект. Но если вы сделаете это, instancetypeкомпилятор знает, какой тип объекта возвращает метод.

Это не академическая проблема. Например, [[NSFileHandle fileHandleWithStandardOutput] writeData:formattedData]сгенерирует ошибку в Mac OS X ( только ). Несколько методов с именем 'writeData:' найдены с несоответствующим результатом, типом параметра или атрибутами . Причина в том, что и NSFileHandle, и NSURLHandle предоставляют writeData:. Так как [NSFileHandle fileHandleWithStandardOutput]возвращает an id, компилятор не уверен, какой класс writeData:вызывается.

Вам нужно обойти это, используя либо:

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

или:

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

Конечно, лучшим решением будет объявить fileHandleWithStandardOutputвозвращение instancetype. Тогда приведение или назначение не является необходимым.

(Обратите внимание, что в iOS этот пример не выдаст ошибку, поскольку только NSFileHandleпредоставляет writeData:там. Существуют и другие примеры, такие как length, который возвращает a CGFloatиз, UILayoutSupportно a NSUIntegerиз NSString.)

Примечание : так как я написал это, заголовки macOS были изменены, чтобы возвращать NSFileHandleвместо вместо id.

Для инициализаторов это сложнее. Когда вы печатаете это:

- (id)initWithBar:(NSInteger)bar

... компилятор сделает вид, что вы набрали это:

- (instancetype)initWithBar:(NSInteger)bar

Это было необходимо для ARC. Это описано в типах результатов Clang Language Extensions . Вот почему люди скажут вам, что нет необходимости использовать instancetype, хотя я утверждаю, что вы должны. Остальная часть этого ответа имеет дело с этим.

Есть три преимущества:

  1. Явный. Ваш код делает то, что говорит, а не что-то еще.
  2. Шаблон. Вы создаете хорошие привычки в те времена, когда это имеет значение, которые существуют.
  3. Согласованность. Вы установили некоторую согласованность с вашим кодом, что делает его более читабельным.

Явный

Это правда, что нет никакой технической выгоды для возвращения instancetypeиз init. Но это потому, что компилятор автоматически преобразует idв instancetype. Вы полагаетесь на эту причуду; в то время как вы пишете, что initвозвращает an id, компилятор интерпретирует его так, как будто он возвращает instancetype.

Это эквивалентно компилятору:

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

Это не эквивалентно вашим глазам. В лучшем случае вы научитесь игнорировать разницу и просматривать ее. Это не то, что вы должны научиться игнорировать.

Шаблон

Хотя нет никакой разницы с initи другими методами, то есть разница , как только вы определяете класс фабрики.

Эти два не эквивалентны:

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

Вы хотите вторую форму. Если вы привыкли печатать instancetypeв качестве возвращаемого типа конструктора, вы каждый раз будете делать это правильно.

консистенция

Наконец, представьте, если вы соберете все это вместе: вам нужна initфункция, а также фабрика классов.

Если вы используете idдля init, вы получите код, подобный этому:

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

Но если вы используете instancetype, вы получите это:

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

Это более последовательно и более читабельно. Они возвращают то же самое, и теперь это очевидно.

Вывод

Если вы намеренно не пишете код для старых компиляторов, вы должны использовать его в instancetypeслучае необходимости.

Вы должны сомневаться, прежде чем писать сообщение, которое возвращается id. Спросите себя: это возвращает экземпляр этого класса? Если это так, это instancetype.

Конечно, есть случаи, когда вам нужно вернуться id, но вы, вероятно, будете использовать их instancetypeгораздо чаще.

Стивен Фишер
источник
4
Прошло много времени с тех пор, как я спросил об этом, и я давно занял позицию, подобную той, которую вы изложили здесь. Я оставил это здесь, потому что я думаю, что, к сожалению, это будет считаться проблемой стиля для init, и информация, представленная в предыдущих ответах, действительно ответила на вопрос довольно четко.
Griotspeak
6
Чтобы быть ясным: я считаю, что ответ Catfish_Man является правильным только в первом предложении . Ответ от hooleyhoop правильный, за исключением первого предложения . Это адская дилемма; Я хотел предоставить что-то, что со временем станет более полезным и более правильным, чем то и другое. (При всем уважении к этим двум; в конце концов, теперь это гораздо более очевидно, чем когда они писали свои ответы.)
Стивен Фишер
1
Со временем надеюсь на это. Я не могу думать ни о какой причине, если только Apple не считает, что важно поддерживать совместимость с исходным кодом, например, назначая новый NSArray для NSString без приведения.
Стивен Фишер
4
instancetypeпротив idдействительно не решение стиля. Недавние изменения instancetypeдействительно проясняют, что мы должны использовать instancetypeв таких местах, -initгде мы подразумеваем «экземпляр моего класса»
griotspeak
2
Вы сказали, что были причины для использования id, не могли бы вы уточнить? Единственные два, о которых я могу думать, это краткость и обратная совместимость; учитывая то, что оба эти вопроса стоят за счет выражения вашего намерения, я думаю, что это действительно плохие аргументы.
Стивен Фишер
10

Приведенных выше ответов более чем достаточно, чтобы объяснить этот вопрос. Я просто хотел бы добавить пример для читателей, чтобы понять его с точки зрения кодирования.

ClassA

@interface ClassA : NSObject

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

@end

Класс б

@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
источник
это сгенерирует ошибку компилятора, только если вы не используете #import "ClassB.h". в противном случае компилятор будет считать, что вы знаете, что делаете. например, следующие компиляции, но происходит сбой во время выполнения: id myNumber = @ (1); id iAssumedThatWasAnArray = myNumber [0]; id iAssumedThatWasADictionary = [myNumber objectForKey: @ "key"];
OlDor
1

Вы также можете получить подробную информацию в назначенном инициализаторе

**

INSTANCETYPE

** Это ключевое слово может использоваться только для типа возвращаемого значения, которое соответствует типу получателя. Метод init всегда объявляется как возвращающий тип экземпляра. Почему бы не сделать, например, возвращаемый тип Party для party party? Это вызвало бы проблему, если бы класс партии был когда-либо подклассом. Подкласс будет наследовать все методы от Party, включая инициализатор и его тип возврата. Если экземпляру подкласса было отправлено это сообщение инициализатора, что будет возвращено? Не указатель на экземпляр Party, а указатель на экземпляр подкласса. Вы можете подумать, что это не проблема, я переопределю инициализатор в подклассе, чтобы изменить тип возвращаемого значения. Но в Objective-C вы не можете иметь два метода с одним и тем же селектором и разными типами возвращаемых данных (или аргументами). Указывая, что метод инициализации возвращает "

МНЕ БЫ

** Перед тем, как тип экземпляра был введен в Objective-C, инициализаторы возвращают id (eye-dee). Этот тип определяется как «указатель на любой объект». (id очень похож на void * в C.) На момент написания, шаблоны классов XCode по-прежнему используют id в качестве возвращаемого типа инициализаторов, добавленных в шаблонный код. В отличие от instancetype, id может использоваться как нечто большее, чем просто возвращаемый тип. Вы можете объявить переменные или параметры метода типа id, если вы не уверены, на какой тип объекта будет указывать переменная. Вы можете использовать id при использовании быстрого перечисления для перебора массива объектов нескольких или неизвестных типов. Обратите внимание: поскольку идентификатор не определен как «указатель на любой объект», вы не включаете * при объявлении переменной или параметра объекта этого типа.

Нам Н. ХУЙНХ
источник