Clang добавляет ключевое слово instancetype
, которое, насколько я вижу, заменяет id
как возвращаемый тип в -alloc
и init
.
Можно ли использовать instancetype
вместо id
?
Clang добавляет ключевое слово instancetype
, которое, насколько я вижу, заменяет id
как возвращаемый тип в -alloc
и init
.
Можно ли использовать instancetype
вместо id
?
Там определенно есть преимущество. Когда вы используете "id", вы практически не проверяете тип. С instancetype компилятор и IDE знают, какой тип вещи возвращается, и лучше проверить ваш код и автозаполнение лучше.
Используйте его только там, где это имеет смысл (т.е. метод, возвращающий экземпляр этого класса); id по-прежнему полезен.
Да, есть преимущества при использовании instancetype
во всех случаях, когда это применимо. Я объясню более подробно, но позвольте мне начать с этого жирного утверждения: Используйте instancetype
всякий раз, когда это уместно, что когда класс возвращает экземпляр этого же класса.
Фактически, вот что сейчас говорит Apple по этому вопросу:
В вашем коде замените вхождения
id
как возвращаемое значение сinstancetype
, где это необходимо. Обычно это относится к методамinit
и классу factory. Несмотря на то, что компилятор автоматически преобразует методы, которые начинаются с "alloc", "init" или "new" и имеют возвращаемый типid
для возвратаinstancetype
, он не конвертирует другие методы. Objective-C соглашение - писатьinstancetype
явно для всех методов.
С этой точки зрения, давайте двигаться дальше и объяснять, почему это хорошая идея.
Во-первых, некоторые определения:
@interface Foo:NSObject
- (id)initWithBar:(NSInteger)bar; // initializer
+ (id)fooWithBar:(NSInteger)bar; // class factory
@end
Для класса factory вы должны всегда использовать instancetype
. Компилятор автоматически не конвертирует id
в instancetype
. Это id
- общий объект. Но если вы сделаете это instancetype
, компилятор знает, какой тип объекта возвращает метод.
Это не академическая проблема. Например, [[NSFileHandle fileHandleWithStandardOutput] writeData:formattedData]
создаст ошибку в Mac OS X (только) Несколько методов с именем "writeData:", найденные с несоответствующим результатом, типом параметра или атрибутами. Причина в том, что NSFileHandle и NSURLHandle предоставляют writeData:
. Поскольку [NSFileHandle fileHandleWithStandardOutput]
возвращает id
, компилятор не уверен в том, что вызывает класс writeData:
.
Вам нужно обойти это, используя либо:
[(NSFileHandle *)[NSFileHandle fileHandleWithStandardOutput] writeData:formattedData];
или
NSFileHandle *fileHandle = [NSFileHandle fileHandleWithStandardOutput];
[fileHandle writeData:formattedData];
Конечно, лучшим решением является объявление fileHandleWithStandardOutput
как возвращающего instancetype
. Тогда приведение или назначение не требуется.
(Обратите внимание, что в iOS этот пример не приведет к ошибке, поскольку только NSFileHandle
предоставляет writeData:
. Существуют другие примеры, такие как length
, который возвращает CGFloat
из UILayoutSupport
, но a NSUInteger
из NSString
.)
Примечание. Поскольку я написал это, заголовки macOS были изменены, чтобы вернуть NSFileHandle
вместо id
.
Для инициализаторов это сложнее. Когда вы наберете это:
- (id)initWithBar:(NSInteger)bar
... компилятор будет притворяться, что вы набрали это вместо этого:
- (instancetype)initWithBar:(NSInteger)bar
Это было необходимо для ARC. Это описано в языковых расширениях Clang Связанные типы результатов. Вот почему люди скажут вам, что нет необходимости использовать instancetype
, хотя я утверждаю, что вы должны. Остальная часть этого ответа касается этого.
Есть три преимущества:
Верно, что нет технической выгоды для возврата instancetype
из init
. Но это связано с тем, что компилятор автоматически преобразует id
в instancetype
. Вы полагаетесь на эту причуду; в то время как вы пишете, что init
возвращает id
, компилятор интерпретирует его так, как будто он возвращает instancetype
.
Они эквивалентны компилятору:
- (id)initWithBar:(NSInteger)bar;
- (instancetype)initWithBar:(NSInteger)bar;
Они не эквивалентны вашим глазам. В лучшем случае вы научитесь игнорировать разницу и избегать ее. Это не то, чему вы должны научиться игнорировать.
Пока нет разницы с init
и другими методами, есть разница, как только вы определяете класс factory.
Эти два не эквивалентны:
+ (id)fooWithBar:(NSInteger)bar;
+ (instancetype)fooWithBar:(NSInteger)bar;
Вам нужна вторая форма. Если вы использовали для ввода instancetype
в качестве возвращаемого типа конструктора, вы будете получать его правильно каждый раз.
Наконец, представьте, если вы соедините все это: вам нужна функция init
, а также класс factory.
Если вы используете id
для init
, вы получите код следующим образом:
- (id)initWithBar:(NSInteger)bar;
+ (instancetype)fooWithBar:(NSInteger)bar;
Но если вы используете instancetype
, вы получите следующее:
- (instancetype)initWithBar:(NSInteger)bar;
+ (instancetype)fooWithBar:(NSInteger)bar;
Это более последовательное и читаемое. Они возвращают то же самое, и теперь это очевидно.
Если вы намеренно пишете код для старых компиляторов, вы должны использовать instancetype
, когда это необходимо.
Вы должны смутиться, прежде чем писать сообщение, которое возвращает id
. Спросите себя: это возврат экземпляра этого класса? Если да, то instancetype
.
Конечно, есть случаи, когда вам нужно вернуть id
, но вы, вероятно, будете использовать instancetype
гораздо чаще.
Выше ответов более чем достаточно, чтобы объяснить этот вопрос. Я просто хотел бы добавить пример для читателей, чтобы понять это с точки зрения кодирования.
ClassA
@interface ClassA : NSObject
- (id)methodA;
- (instancetype)methodB;
@end
Класс 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
}
Вы также можете получить подробную информацию в Назначенный инициализатор
**
** Это ключевое слово может использоваться только для типа возврата, которое соответствует типу возвращаемого ресивера. метод init всегда объявляется для возврата типа instancetype. Например, почему бы не сделать партию возвращаемого типа для партийного экземпляра? Это вызовет проблему, если класс партии будет когда-либо подклассифицирован. Подкласс наследует все методы от Стороны, включая инициализатор и его тип возврата. Если экземпляр подкласса был отправлен это сообщение инициализатора, это будет возврат? Не указатель на экземпляр партии, а указатель на экземпляр подкласса. Вы можете подумать, что нет проблем, я переопределю инициализатор в подклассе, чтобы изменить тип возврата. Но в Objective-C у вас не может быть двух методов с одним и тем же селектором и разными типами возврата (или аргументами). Указав, что метод инициализации возвращает "экземпляр принимающего объекта", вам никогда не придется беспокоиться о том, что происходит в этой ситуации. **
** До того, как instancetype был введен в Objective-C, инициализаторы возвращают id (eye-dee). Этот тип определяется как "указатель на любой объект". (id во многом похож на void * на C.). На момент написания шаблонов класса XCode по-прежнему используется id в качестве возвращаемого типа инициализаторов, добавленных в шаблонный код. В отличие от instancetype, id может использоваться как больше, чем просто тип возврата. Вы можете объявлять переменные или параметры метода идентификатора типа, когда вы не знаете, какой тип объекта будет указывать на переменную. Вы можете использовать id при использовании быстрого перечисления для итерации по массиву из нескольких или неизвестных типов объектов. Обратите внимание, что поскольку id undefined как "указатель на любой объект", вы не включаете * при объявлении переменной или объектного параметра этого типа.