В версию 5.0 Xcode (LLVM 5/Clang 5) была добавлена поддержка нового ключевого слова для Objective-C — «instancetype».
Для чего оно нужно?
В соответствии с принятыми в Cocoa соглашениями наименования методов, Objective-C методы с названиями начинающимися с «init», «alloc», «copy», а также методы с названиями «new», «autorelease», «retain», «self» должны возвращать экземпляр класса которому они принадлежат. Такие методы возвращают связанный тип. Послав сообщение одному из этих методов, в ответ мы получим экземпляр класса, которому принадлежит метод.
Рассмотрим небольшой пример:
// Test Class @interface TestClass: NSObject +(id) instance; @end @implementation TestClass + (id) instance { return [[self alloc] init]; } @end // Other Test Class @interface OtherTestClass: TestClass -(void) doSomething; @end @implementation OtherTestClass -(void) doSomething { // ... } @end
В примере мы создали класс «TestClass», который реализует метод «instance», и унаследованный от него класс «OtherTestClass», реализующий метод «doSomething».
Теперь пошлем сообщение «doSomething» экземпляру «TestClass»:
[[[TestClass alloc] init] doSomething]; // Ошибка: No visible @interface for 'TestClass' declares the selector 'doSomething' [[TestClass instance] doSomething]; // Ошибки нет!
При выполнении первой инструкции компилятор (тут и далее по тексту имеется ввиду и компилятор, и статический анализатор) ругнется на неизвестный селектор.
Это происходит потому, что компилятор осуществляет проверку на соответствие возвращаемого типа, базируясь на соглашении наименования методов. Вызов «[TestClass alloc]» возвращает новый экземпляр класса «TestClass», так как «alloc» неявно возвращает связанный тип. Точно также «[[TestClass alloc] init]» возвращает экземпляр класса «TestClass». Зная, что результатом вызова «[[TestClass alloc] init]» является экземпляр класса «TestClass», компилятор проверяет наличие селектора «doSomething».
Выполняя вторую инструкцию, компилятор не ругнется! Как указано в сигнатуре, метод «instance», также как «alloc» и «init», возвращает тип «id». Но в данном случаем проверка возвращаемого типа компилятором не производится, так как название метода не попадает под соглашение наименования и его возвращаемый тип интерпритируется как «id», т.е. возвращаемый тип соответствует указанному в сигнатуре типу «id». В такой ситуации приложение аварийно завершится.
Для решения этой проблемы можно использовать явное приведение типов:
// Все OK. Как и положено компилятор генерирует ошибку: // No visible @interface for 'TestClass' declares the selector 'doSomething' [(TestClass *)[TestClass instance] doSomething];
Теперь компилятор знает тип и проверяет существование селектора.
Также можно в объявлении метода явно указать возвращаемый тип как «TestClass *»:
+(TestClass *) instance;
Компилятор знает возвращаемый тип и выполняет проверку существования селектора. Но тут у нас возникает проблема с наследованием:
// Test Class @interface TestClass: NSObject +(TestClass *) instance; @end @implementation TestClass + (TestClass *) instance { return [[self alloc] init]; } @end // Other Test Class @interface OtherTestClass: TestClass -(void) doSomething; @end @implementation OtherTestClass -(void) doSomething { // ... } @end //... [[OtherTestClass instance] doSomething]; //Ошибка: No visible @interface for 'TestClass' declares the selector 'doSomething'
Так как «instance» возвращает экземпляр класса «TestClass», у которого нет селектора «doSomething», то компилятор генерирует ошибку. Для решения данной проблемы нам необходимо в классе «OtherTestClass» переопределить метод «instance»:
// Other Test Class @interface OtherTestClass: TestClass -(void) doSomething; +(OtherTestClass *) instance; @end @implementation OtherTestClass -(void) doSomething { // ... } + (OtherTestClass *) instance { return [[self alloc] init]; } @end
Рассмотрим еще один пример. Допустим в классе «OtherTestClass» мы перегрузим метод «instance», которые возвращает тип «id»:
// Test Class @interface TestClass: NSObject +(id) instance; @end @implementation TestClass + (id) instance { return [[self alloc] init]; } @end // Other Test Class @interface OtherTestClass: TestClass -(void) doSomething; @end @implementation OtherTestClass -(void) doSomething { // ... } +(NSString *) instance { return [NSString stringWithFormat:@"Hello"]; } @end [[OtherTestClass instance] doSomething];
В данном случае компилятор также ничего не скажет, так как возвращаемое значение будет интерпритироваться как «id». Но вот во время выполнения приложения мы получим ошибку:
-[__NSCFString doSomething]: unrecognized selector sent to instance 0x102302a20
Приложение аварийно завершится.
Для того, чтобы избежать подобных проблем, был введет специальный тип «instancetype», который дает знать компилятору, что возвращаемый тип должен быть экземпляром этого же класса и, в отличие от «id», может быть использован только в качестве возвращаемого типа в объявлении метода.
Посмотрим на пример:
@interface TestClass: NSObject +(instancetype) instance; @end @implementation TestClass + (instancetype) instance { return [[self alloc] init]; } @end @interface OtherTestClass: TestClass -(void) doSomething; @end @implementation OtherTestClass -(void) doSomething { // ... } @end // .. // Ошибка: No visible @interface for 'TestClass' declares the selector 'doSomething' [[[TestClass alloc] init] doSomething]; // // Ошибка: No visible @interface for 'TestClass' declares the selector 'doSomething' [[TestClass instance] doSomething];
Как видим, компилятор сгенерировал ошибку при выполнении «[[TestClass instance] doSomething]», так как с помощью «instancetype» мы указали, что метод должен возвращать тип экземпляра класса.
Если дочерний класс переопределят метод, возвращаемый тип «instancetype», то он должен возвращать экземпляр своего класса, а если это не так, то мы получим предупреждение:
@implementation OtherTestClass -(void) doSomething { } // Method is expected to return an instance of its class type 'OtherTestClass', but is declared to return 'NSString *' +(NSString *) instance { return [NSString stringWithFormat:@"Hello"]; } @end
или
@implementation OtherTestClass -(void) doSomething { //... } +(instancetype) instance { // Incompatible pointer types returning 'NSString *' from a function with result type 'OtherTestClass *' return [NSString stringWithFormat:@"Hello"]; } @end
Ключевое слово «instancetype» стоит использовать в качестве возвращаемого значения методов если:
- метод возвращает только экземпляр своего класса и он является методом класса
- метод начинается с «init», «autorelease», «retain», или «self» и является методом экземпляра
- метод начинается с «new» или «alloc» и является методом класса
Ссылки:
Автор: Антон Добкин