Уже довольно давно в Xсode есть возможность проверить свой код на соответствие современным особенностям Objective-C (Edit > Refactor > Convert to Modern Objective-C Syntax…). Мне всегда было интересно наблюдать за тем, что Apple продвигает в качестве хорошей практики; и даже если вы не доверяете Xcode автоматически изменять код, это простой способ проверить его на возможность внесения потенциальных улучшений.
Xcode 6 представляет несколько нововведений, а кроме того, гораздо большую гибкость, позволяя самостоятельно контролировать, какие преобразования запускать:
К сожалению, из описания преобразования не всегда очевидно, что оно делает. Некоторые полезные подробности можно прочитать в руководстве Adopting Modern Objective-C а также посмотреть на WWDC 2014 Session 417 What’s New in LLVM. Эта статья содержит мои заметки по каждому из преобразований.
Синтаксис @property
Едва ли введение нового синтаксиса для свойств можно назвать новостью. Xcode 6 расширил список нововведений, добавив две опции для обнаружения свойств вместе с контролем их атомарности.
- Infer readonly properties (по умолчанию Yes)
- Infer readwrite properties (по умолчанию Yes)
При выборе этих опций будет осуществлён поиск недостающего объявления @property
путём определения потенциальных getter и setter методов в классе. К примеру, для такого класса с двумя методами без соответствующего свойства:
- (NSString *)name;
- (void)setName:(NSString *)newName;
Xcode сделает вывод о необходимости свойства и добавит его в интерфейс класса:
@property (nonatomic, copy) NSString *name;
Объявление свойства явно показывает назначение двух методов и позволяет компилятору автоматически синтезировать акцессор. Тут следует быть осторожным, потому что два существующих метода не будут автоматически удалены. Если в них описано нестандартное поведение, то это может быть опасным. Кроме того, Xcode может перестараться и предложить свойство для методов, которые не являются getter или setter методами, что делает это преобразование менее полезным.
- Atomicity of inferred properties (по умолчанию NS_NONATOMIC_IOSONLY)
Эта опция позволяет вам выбрать, хотите ли вы, чтобы при создании объявления только что обнаруженного свойства, оно было atomic
, nonatomic
или был использован макрос NS_NONATOMIC_IOSONLY
. Последнее есть ни что иное, как макрос, который принимает значение nonatomic
для iOS и ничего не делает для OS X. Если вы пишете код для обоих систем, это вам пригодится. Иначе, в большинстве случаев, стоит остановиться на nonatomic
.
@property (NS_NONATOMIC_IOSONLY, readonly, copy) NSDate *dueDate;
Назначенный конструктор (designated Initializer)
Преобразование Infer designated initializer methods идентифицирует и помечает назначенные конструкторы с помощью NS_DESIGNATED_INITIALIZER
. Чтобы понять, что это такое и для чего это нужно, стоит вкратце вспомнить, как работает инициализация объекта в Objective-C. Создание объекта в Objective-C проходит в два шага: выделение памяти, а затем инициализация. Обычно их записывают в одну строку:
MyObject *object = [[MyObject alloc] init];
Метод инициализации отвечает как за установку значения для любой instance-переменной, так и за все остальные начальные задачи для объекта. У класса может быть много методов инициализации, которые по соглашению начинаются с префикса init
. Например, у класса с instance-переменной name
всегда должна быть определена, может быть метод инициализации, который включает в себя имя:
- (instancetype)init
{
return [self initWithName:@"Unknown"];
}
- (instancetype)initWithName:(NSString *)name
{
self = [super init];
if (self)
{
_name = [name copy];
}
return self;
}
Простой init в этом случае — удобный (convenience) конструктор, которой просто вызывает назначенный (designated) конструктор initWithName:
, используя в качестве параметра значение по умолчанию. Назначенный метод гарантирует полную инициализацию объекта, вызывая унаследованный init
.
Если же теперь у этого класса появится наследник, то наследнику станут важны детали реализации суперкласса. Правила для назначенных конструкторов:
- назначенный конструктор. должен вызывать (через super) назначенный конструктор суперкласса. Для класса, наследующегося от NSObject, это будет просто
[super init]
. - Любой удобный конструктор должен вызывать другой конструктор в своём классе, который в конце-концов приведёт к назначенному конструктору.
- Класс с назначенным конструктором должен реализовывать все назначенные конструкторы суперкласса.
Долгое время не было способа показать компилятору или тому, кто использует класс, какой из методов инициализации является назначенным (кроме как в комментарии). Теперь, чтобы исправить эту ситуацию, в Clang есть атрибут objc_designated_initializer
. А в iOS 8 есть макрос NS_DESIGNATED_INITIALIZER
, определённый в NSObjCRuntime.h, который позволяет легче применить этот атрибут к методу:
#define NS_DESIGNATED_INITIALIZER __attribute__((objc_designated_initializer))
Рассмотренный ранее пример в этом случае можно записать так:
- (instancetype)init;
- (instancetype)initWithName:(NSString *)name NS_DESIGNATED_INITIALIZER;
Теперь, если вы добавили удобный конструктор, который не вызывает назначенный конструктор, то получите предупреждение.
Я видел, что люди сообщали о проблемах с некоторыми классами UIKit, в которых Apple ещё не пометила назначенные конструкторы, так что, как обычно, не помешает провести тестирование и отправить отчёт об ошибке в случае непредвиденных результатов.
Infer Instancetype for Method Result Type
Позволяет заменить id
на instancetype
в качестве возвращаемого типа для методов alloc
, init
и new
. Factory-методы класса возможно вам придётся изменять вручную. Более детально о том, как повысить надёжность кода, применяя instancetype
, можно прочитать в статье NSHipster.
Infer Protocol Conformance
Это преобразование, отключённое по умолчанию, позволяет Xcode добавить отсутствующее объявление поддержки протокола. К примеру, вот простой контроллер, не заявляющий о поддержке какого-либо протокола:
@interface UYLViewController : UIViewController
Если этот класс реализует два обязательных метода для протокола UITableViewDataSource -tableView:numberOfRowsInSection:
и -tableView:cellForRowAtIndexPath:
, то описание интерфейса будет изменено следующим образом:
@interface UYLViewController : UIViewController<UITableViewDataSource>
От себя могу добавить, что так происходит только в том случае, если реализованы все обязательные методы. У меня не получилось добиться, например, добавления поддержки протокола UITableViewDelegate, если были реализованы только опциональные методы.
Литералы Objective-C
Литералы и индексирование Objective-C уже были представлены ранее в Xcode, так что я просто приведу быстрый пример:
NSNumber *magicNumber = [NSNumber numberWithInteger:42];
NSDictionary *myDictionary = [NSDictionary dictionaryWithObject:magicNumber forKey:@"magic"];
Рефакторинг с использованием литералов и индексирования Objective-C приводит к более компактному коду:
NSNumber *magicNumber = @42;
NSDictionary *myDictionary = @{@"magic": magicNumber};
Перечисления
Макросы NS_ENUM
и NS_OPTIONS
.
Современные макросы NS_ENUM
и NS_OPTIONS
— это быстрый и лёгкий способ создания перечислений с указанием типа и размера компилятору. Например, перечисляемый тип:
enum
{
UYLTypeDefault,
UYLTypeSmall,
UYLTypeLarge
};
typedef NSInteger UYLType;
после рефакторинга с использованием NS_ENUM
превращается в:
typedef NS_ENUM(NSInteger, UYLType)
{
UYLTypeDefault,
UYLTypeSmall,
UYLTypeLarge
};
Подобным образом набор битовых масок:
enum
{
UYLBitMaskA = 0,
UYLBitMaskB = 1 << 0,
UYLBitMaskC = 1 << 1,
UYLBitMaskD = 1 << 2
};
typedef NSUInteger UYLBitMask;
после рефакторинга с использованием NS_OPTIONS
превращается в:
typedef NS_OPTIONS(NSUInteger, UYLBitMask)
{
UYLBitMaskA = 0,
UYLBitMaskB = 1 << 0,
UYLBitMaskC = 1 << 1,
UYLBitMaskD = 1 << 2
};
Add attribute annotations
Не смог добиться какого-либо результата, выбрав эту опцию. В пояснении предполагается, что будут добавлены аннотации к свойствам и методам, но у меня не получилось определить, какие атрибуты будут добавлены и при каких условиях. Оставьте комментарий, если знаете…
Автор: emankin