Большинство iOS-проектов частично или полностью переходят на Swift. Swift — замечательный язык, и за ним будущее разработки под iOS. Но язык нераздельно связан с инструментарием, а в инструментарии Swift есть недостатки.
В компиляторе Swift по-прежнему находятся баги, которые приводят к его падению или генерации неправильного кода. У Swift нет стабильного ABI. И, что очень важно, проекты на Swift собираются слишком долго.
В связи с этим существующим проектам может быть выгоднее продолжать разработку на Objective-C. А Objective-C уже не тот, что был раньше!
В этом цикле статей мы покажем полезные возможности и улучшения Objective-C, с которыми писать код становится намного приятнее. Каждый, кто пишет на Objective-C, найдет для себя что-нибудь интересное.
let
и var
В Objective-C больше не нужно явно указывать типы переменных: еще в Xcode 8 появилось расширение языка __auto_type
, а до Xcode 8 выведение типов было доступно в Objective-C++ (при помощи ключевого слова auto
с появлением C++0X).
Для начала добавим макросы let
и var
:
#define let __auto_type const
#define var __auto_type
// Было
NSArray<NSString *> *const items = [string componentsSeparatedByString:@","];
void(^const completion)(NSData * _Nullable, NSURLResponse * _Nullable, NSError * _Nullable) = ^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
// ...
};
// Стало
let items = [string componentsSeparatedByString:@","];
let completion = ^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
// ...
};
Если раньше писать const
после указателя на Objective-C класс было непозволительной роскошью, то теперь неявное указание const
(через let
) стало само собой разумеющимся. Особенно заметна разница при сохранении блока в переменную.
Для себя мы выработали правило использовать let
и var
для объявления всех переменных. Даже когда переменная инициализируется значением nil
:
- (nullable JMSomeResult *)doSomething {
var result = (JMSomeResult *)nil;
if (...) {
result = ...;
}
return result;
}
Единственное исключение — когда надо гарантировать, что переменной присваивается значение в каждой ветке кода:
NSString *value;
if (...) {
if (...) {
value = ...;
} else {
value = ...;
}
} else {
value = ...;
}
Только таким образом мы получим предупреждение компилятора, если забудем присвоить значение в какой-то из веток.
И напоследок: чтобы использовать let
и var
для переменных типа id
, нужно отключить предупреждение auto-var-id
(добавить -Wno-auto-var-id
в "Other Warning Flags" в настройках проекта).
Автовывод типа возвращаемого значения блока
Немногие знают, что компилятор умеет выводить тип возвращаемого значения блока:
let block = ^{
return @"abc";
};
// `block` имеет тип `NSString *(^const)(void)`
Это очень удобно. Особенно если вы пишете "реактивный" код с использованием ReactiveObjC. Но есть ряд ограничений, при которых нужно явно указывать тип возвращаемого значения.
- Если в блоке есть несколько операторов
return
, возвращающих значения разных типов.
let block1 = ^NSUInteger(NSUInteger value){
if (value > 0) {
return value;
} else {
// `NSNotFound` имеет тип `NSInteger`
return NSNotFound;
}
};
let block2 = ^JMSomeBaseClass *(BOOL flag) {
if (flag) {
return [[JMSomeBaseClass alloc] init];
} else {
// `JMSomeDerivedClass` наследуется от `JMSomeBaseClass`
return [[JMSomeDerivedClass alloc] init];
}
};
- Если в блоке есть оператор
return
, возвращающийnil
.
let block1 = ^NSString * _Nullable(){
return nil;
};
let block2 = ^NSString * _Nullable(BOOL flag) {
if (flag) {
return @"abc";
} else {
return nil;
}
};
- Если блок должен возвращать
BOOL
.
let predicate = ^BOOL(NSInteger lhs, NSInteger rhs){
return lhs > rhs;
};
Выражения с оператором сравнения в языке C (и, следовательно, в Objective-C) имеют тип int
. Поэтому лучше взять за правило всегда явно указывать возвращаемый тип BOOL
.
Generics и for...in
В Xcode 7 в Objective-C появились generics (точнее, lightweight generics). Надеемся, что вы их уже используете. Но если нет, то можно посмотреть сессию WWDC или прочитать здесь или здесь.
Мы для себя выработали правило всегда указывать generic-параметры, даже если это id
(NSArray<id> *
). Таким образом можно легко отличить legacy-код, в котором generic-параметры еще не указаны.
Имея макросы let
и var
, мы ожидаем, что сможем использовать их в цикле for...in
:
let items = (NSArray<NSString *> *)@[@"a", @"b", @"c"];
for (let item in items) {
NSLog(@"%@", item);
}
Но такой код не скомпилируется. Скорее всего, __auto_type
не стали поддерживать в for...in
, потому что for...in
работает только с коллекциями, реализующими протокол NSFastEnumeration
. А для протоколов в Objective-C нет поддержки generics.
Чтобы исправить этот недостаток, попробуем сделать свой макрос foreach
. Первое, что приходит в голову: у всех коллекций в Foundation есть свойство objectEnumerator
, и макрос мог бы выглядеть так:
#define foreach(object_, collection_)
for (typeof([(collection_).objectEnumerator nextObject]) object_ in (collection_))
Но для NSDictionary
и NSMapTable
метод протокола NSFastEnumeration
итерируется по ключам, а не по значениям (нужно было бы использовать keyEnumerator
, а не objectEnumerator
).
Нам понадобится объявить новое свойство, которое будет использоваться только для получения типа в выражении typeof
:
@interface NSArray<__covariant ObjectType> (ForeachSupport)
@property (nonatomic, strong, readonly) ObjectType jm_enumeratedType;
@end
@interface NSDictionary<__covariant KeyType, __covariant ObjectType> (ForeachSupport)
@property (nonatomic, strong, readonly) KeyType jm_enumeratedType;
@end
#define foreach(object_, collection_)
for (typeof((collection_).jm_enumeratedType) object_ in (collection_))
Теперь наш код выглядит намного лучше:
// Было
for (MyItemClass *item in items) {
NSLog(@"%@", item);
}
// Стало
foreach (item, items) {
NSLog(@"%@", item);
}
foreach (<#object#>, <#collection#>) {
<#statements#>
}
Generics и copy
/mutableCopy
Еще одно место, где в Objective-C отсутствует типизация, — это методы -copy
и -mutableCopy
(а также методы -copyWithZone:
и -mutableCopyWithZone:
, но их мы не вызываем напрямую).
Чтобы избежать необходимости явного приведения типов, можно переобъявить методы с указанием возвращаемого типа. Например, для NSArray
объявления будут такими:
@interface NSArray<__covariant ObjectType> (TypedCopying)
- (NSArray<ObjectType> *)copy;
- (NSMutableArray<ObjectType> *)mutableCopy;
@end
let items = [NSMutableArray<NSString *> array];
// ...
// Было
let itemsCopy = (NSArray<NSString *> *)[items copy];
// Стало
let itemsCopy = [items copy];
warn_unused_result
Раз уж мы переобъявили методы -copy
и -mutableCopy
, было бы неплохо гарантировать, что результат вызова этих методов будет использован. Для этого в Clang есть атрибут warn_unused_result
.
#define JM_WARN_UNUSED_RESULT __attribute__((warn_unused_result))
@interface NSArray<__covariant ObjectType> (TypedCopying)
- (NSArray<ObjectType> *)copy JM_WARN_UNUSED_RESULT;
- (NSMutableArray<ObjectType> *)mutableCopy JM_WARN_UNUSED_RESULT;
@end
Для следующего кода компилятор сгенерирует предупреждение:
let items = @[@"a", @"b", @"c"];
[items mutableCopy]; // Warning: Ignoring return value of function declared with 'warn_unused_result' attribute.
overloadable
Немногие знают, что Clang позволяет переопределять функции в языке C (а следовательно, и в Objective-C). C помощью атрибута overloadable
можно создавать функции с одинаковым названием, но с разными типами аргументов или с их разным количеством.
Переопределяемые функции не могут отличаться только лишь типом возвращаемого значения.
#define JM_OVERLOADABLE __attribute__((overloadable))
JM_OVERLOADABLE float JMCompare(float lhs, float rhs);
JM_OVERLOADABLE float JMCompare(float lhs, float rhs, float accuracy);
JM_OVERLOADABLE double JMCompare(double lhs, double rhs);
JM_OVERLOADABLE double JMCompare(double lhs, double rhs, double accuracy);
Boxed expressions
В далеком 2012 году в сессии WWDC 413 Apple представила литералы для NSNumber
, NSArray
и NSDictionary
, а также boxed expressions. Подробно о литералах и boxed expressions можно прочитать в документации Clang.
// Литералы
@YES // [NSNumber numberWithBool:YES]
@NO // [NSNumber numberWithBool:NO]
@123 // [NSNumber numberWithInt:123]
@3.14 // [NSNumber numberWithDouble:3.14]
@[obj1, obj2] // [NSArray arrayWithObjects:obj1, obj2, nil]
@{key1: obj1, key2: obj2} // [NSDictionary dictionaryWithObjectsAndKeys:obj1, key1, obj2, key2, nil]
// Boxed expressions
@(boolVariable) // [NSNumber numberWithBool:boolVariable]
@(intVariable) // [NSNumber numberWithInt:intVariable)]
С помощью литералов и boxed expressions можно легко получить объект, представляющий число или булево значение. Но чтобы получить объект, оборачивающий структуру, нужно написать немного кода:
// Оборачивание `NSDirectionalEdgeInsets` в `NSValue`
let insets = (NSDirectionalEdgeInsets){ ... };
let value = [[NSValue alloc] initWithBytes:&insets objCType:@encode(typeof(insets))];
// ...
// Получение `NSDirectionalEdgeInsets` из `NSValue`
var insets = (NSDirectionalEdgeInsets){};
[value getValue:&insets];
Для некоторых классов определены вспомогательные методы и свойства (наподобие метода +[NSValue valueWithCGPoint:]
и свойства CGPointValue
), но это все равно не так удобно, как boxed expression!
И в 2015 году Алекс Денисов сделал патч для Clang, позволяющий использовать boxed expressions для оборачивания любых структур в NSValue
.
Чтобы наша структура поддерживала boxed expressions, нужно просто добавить атрибут objc_boxable
для структуры.
#define JM_BOXABLE __attribute__((objc_boxable))
typedef struct JM_BOXABLE JMDimension {
JMDimensionUnit unit;
CGFloat value;
} JMDimension;
И мы можем использовать синтаксис @(...)
для нашей структуры:
let dimension = (JMDimension){ ... };
let boxedValue = @(dimension); // Имеет тип `NSValue *`
Получать структуру обратно по-прежнему придется через метод -[NSValue getValue:]
или метод категории.
В CoreGraphics определен свой макрос CG_BOXABLE
, и boxed expressions уже поддержаны для структур CGPoint
, CGSize
, CGVector
и CGRect
.
Для остальных часто используемых структур мы можем добавить поддержку boxed expressions самостоятельно:
typedef struct JM_BOXABLE _NSRange NSRange;
typedef struct JM_BOXABLE CGAffineTransform CGAffineTransform;
typedef struct JM_BOXABLE UIEdgeInsets UIEdgeInsets;
typedef struct JM_BOXABLE NSDirectionalEdgeInsets NSDirectionalEdgeInsets;
typedef struct JM_BOXABLE UIOffset UIOffset;
typedef struct JM_BOXABLE CATransform3D CATransform3D;
Compound literals
Еще одна полезная конструкция языка — compound literal. Compound literals появились еще в GCC в виде расширения языка, а позже были добавлены в стандарт C11.
Если раньше, встретив вызов UIEdgeInsetsMake
, мы могли только гадать, какие отступы мы получим (надо было смотреть объявление функции UIEdgeInsetsMake
), то с compound literals код говорит сам за себя:
// Было
UIEdgeInsetsMake(1, 2, 3, 4)
// Стало
(UIEdgeInsets){ .top = 1, .left = 2, .bottom = 3, .right = 4 }
Еще удобнее использовать такую конструкцию, когда часть полей равны нулю:
(CGPoint){ .y = 10 }
// вместо
(CGPoint){ .x = 0, .y = 10 }
(CGRect){ .size = { .width = 10, .height = 20 } }
// вместо
(CGRect){ .origin = { .x = 0, .y = 0 }, .size = { .width = 10, .height = 20 } }
(UIEdgeInsets){ .top = 10, .bottom = 20 }
// вместо
(UIEdgeInsets){ .top = 20, .left = 0, .bottom = 10, .right = 0 }
Конечно, в compound literals можно использовать не только константы, но и любые выражения:
textFrame = (CGRect){
.origin = {
.y = CGRectGetMaxY(buttonFrame) + textMarginTop
},
.size = textSize
};
(NSRange){ .location = <#location#>, .length = <#length#> }
(CGPoint){ .x = <#x#>, .y = <#y#> }
(CGSize){ .width = <#width#>, .height = <#height#> }
(CGRect){
.origin = {
.x = <#x#>,
.y = <#y#>
},
.size = {
.width = <#width#>,
.height = <#height#>
}
}
(UIEdgeInsets){ .top = <#top#>, .left = <#left#>, .bottom = <#bottom#>, .right = <#right#> }
(NSDirectionalEdgeInsets){ .top = <#top#>, .leading = <#leading#>, .bottom = <#bottom#>, .trailing = <#trailing#> }
(UIOffset){ .horizontal = <#horizontal#>, .vertical = <#vertical#> }
Nullability
В Xcode 6.3.2 в Objective-C появились nullability-аннотации. Разработчики Apple добавили их для импортирования Objective-C API в Swift. Но если что-то добавлено в язык, то надо постараться поставить это себе на службу. И мы расскажем, как используем nullability в Objective-C проекте и какие есть ограничения.
Чтобы освежить знания, можно посмотреть сессию WWDC.
Первое, что мы сделали, — это начали писать макросы NS_ASSUME_NONNULL_BEGIN
/ NS_ASSUME_NONNULL_END
во всех .m
-файлах. Чтобы не делать этого руками, мы патчим шаблоны файлов прямо в Xcode.
Мы стали также правильно расставлять nullability для всех приватных свойств и методов.
Если мы добавляем макросы NS_ASSUME_NONNULL_BEGIN
/ NS_ASSUME_NONNULL_END
в уже существующий .m
-файл, то сразу дописываем недостающие nullable
, null_resettable
и _Nullable
во всем файле.
Все полезные предупреждения компилятора, связанные с nullability, включены по умолчанию. Но есть одно экстремальное предупреждение, которое хотелось бы включить: -Wnullable-to-nonnull-conversion
(задается в "Other Warning Flags" в настройках проекта). Компилятор выдает это предупреждение, когда переменная или выражение с nullable-типом неявно приводится к nonnull-типу.
+ (NSString *)foo:(nullable NSString *)string {
return string; // Implicit conversion from nullable pointer 'NSString * _Nullable' to non-nullable pointer type 'NSString * _Nonnull'
}
К сожалению, для __auto_type
(а следовательно, и let
и var
) это предупреждение не срабатывает. В типе, выведенном через __auto_type
, отбрасывается nullability-аннотация. И, судя по комментарию разработчика Apple в rdar://27062504, это поведение уже не изменится. Экспериментально замечено, что добавление _Nullable
или _Nonnull
к __auto_type
ни на что не влияет.
- (NSString *)test:(nullable NSString *)string {
let tmp = string;
return tmp; // Нет предупреждения
}
Для подавления предупреждения nullable-to-nonnull-conversion
мы написали макрос, который делает "force unwrap". Идея взята из макроса RBBNotNil
. Но за счет поведения __auto_type
удалось избавиться от вспомогательного класса.
#define JMNonnull(obj_)
({
NSCAssert(obj_, @"Expected `%@` not to be nil.", @#obj_);
(typeof({ __auto_type result_ = (obj_); result_; }))(obj_);
})
Пример использования макроса JMNonnull
:
@interface JMRobot : NSObject
@property (nonatomic, strong, nullable) JMLeg *leftLeg;
@property (nonatomic, strong, nullable) JMLeg *rightLeg;
@end
@implementation JMRobot
- (void)stepLeft {
[self step:JMNonnull(self.leftLeg)]
}
- (void)stepRight {
[self step:JMNonnull(self.rightLeg)]
}
- (void)step:(JMLeg *)leg {
// ...
}
@end
Отметим, что на момент написания статьи предупреждение nullable-to-nonnull-conversion
работает неидеально: компилятор пока не понимает, что nullable
-переменную после проверки на неравенство nil
можно воспринимать как nonnull
.
- (NSString *)foo:(nullable NSString *)string {
if (string != nil) {
return string; // Implicit conversion from nullable pointer 'NSString * _Nullable' to non-nullable pointer type 'NSString * _Nonnull'
} else {
return @"";
}
}
В Objective-C++ коде можно обойти это ограничение, использовав конструкцию if let
, поскольку Objective-C++ допускает объявление переменных в выражении оператора if
.
- (NSString *)foo:(nullable NSString *)stringOrNil {
if (let string = stringOrNil) {
return string;
} else {
return @"";
}
}
Полезные ссылки
Есть также ряд более известных макросов и ключевых слов, которые хотелось бы упомянуть: ключевое слово @available
, макросы NS_DESIGNATED_INITIALIZER
, NS_UNAVAILABLE
, NS_REQUIRES_SUPER
, NS_NOESCAPE
, NS_ENUM
, NS_OPTIONS
(или свои макросы для тех же атрибутов) и макрос @keypath
из библиотеки libextobjc. Советуем также посмотреть остальные возможности библиотеки libextobjc.
→ Код для статьи выложен в gist.
Заключение
В первой части статьи мы постарались рассказать об основных возможностях и простых улучшениях языка, которые существенно облегчают написание и поддержку Objective-C кода. В следующей части мы покажем, как можно еще увеличить свою продуктивность с помощью enum'ов как в Swift (они же Case-классы; они же Алгебраические типы данных, ADT) и возможности реализации методов на уровне протокола.
Автор: artyom_stv