- PVSM.RU - https://www.pvsm.ru -
Большинство 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 [1]. Но есть ряд ограничений, при которых нужно явно указывать тип возвращаемого значения.
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.
for...inВ Xcode 7 в Objective-C появились generics (точнее, lightweight generics). Надеемся, что вы их уже используете. Но если нет, то можно посмотреть сессию WWDC [2] или прочитать здесь [3] или здесь [4].
Мы для себя выработали правило всегда указывать 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#>
}
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 [5].
#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 [6] можно создавать функции с одинаковым названием, но с разными типами аргументов или с их разным количеством.
Переопределяемые функции не могут отличаться только лишь типом возвращаемого значения.
#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);
В далеком 2012 году в сессии WWDC 413 [7] Apple представила литералы для NSNumber, NSArray и NSDictionary, а также boxed expressions. Подробно о литералах и boxed expressions можно прочитать в документации Clang [8].
// Литералы
@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 году Алекс Денисов [9] сделал патч [10] для Clang, позволяющий использовать boxed expressions для оборачивания любых структур в NSValue.
Чтобы наша структура поддерживала boxed expressions, нужно просто добавить атрибут objc_boxable [11] для структуры.
#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 literal [12]. 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#> }
В Xcode 6.3.2 в Objective-C появились nullability-аннотации [13]. Разработчики Apple добавили их для импортирования Objective-C API в Swift. Но если что-то добавлено в язык, то надо постараться поставить это себе на службу. И мы расскажем, как используем nullability в Objective-C проекте и какие есть ограничения.
Чтобы освежить знания, можно посмотреть сессию WWDC [2].
Первое, что мы сделали, — это начали писать макросы 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 [14], это поведение уже не изменится. Экспериментально замечено, что добавление _Nullable или _Nonnull к __auto_type ни на что не влияет.
- (NSString *)test:(nullable NSString *)string {
let tmp = string;
return tmp; // Нет предупреждения
}
Для подавления предупреждения nullable-to-nonnull-conversion мы написали макрос, который делает "force unwrap". Идея взята из макроса RBBNotNil [15]. Но за счет поведения __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 [16], макросы NS_DESIGNATED_INITIALIZER, NS_UNAVAILABLE, NS_REQUIRES_SUPER, NS_NOESCAPE, NS_ENUM, NS_OPTIONS (или свои макросы для тех же атрибутов) и макрос @keypath [17] из библиотеки libextobjc. Советуем также посмотреть остальные возможности библиотеки libextobjc [18].
→ Код для статьи выложен в gist [23].
В первой части статьи мы постарались рассказать об основных возможностях и простых улучшениях языка, которые существенно облегчают написание и поддержку Objective-C кода. В следующей части мы покажем, как можно еще увеличить свою продуктивность с помощью enum'ов как в Swift (они же Case-классы; они же Алгебраические типы данных [24], ADT) и возможности реализации методов на уровне протокола.
Автор: artyom_stv
Источник [25]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/ios/300399
Ссылки в тексте:
[1] ReactiveObjC: https://github.com/ReactiveCocoa/ReactiveObjC
[2] сессию WWDC: https://developer.apple.com/videos/play/wwdc2015/401/
[3] здесь: https://medium.com/ios-os-x-development/generics-in-objective-c-8f54c9cfbce7
[4] здесь: https://useyourloaf.com/blog/using-objective-c-lightweight-generics/
[5] warn_unused_result: https://clang.llvm.org/docs/AttributeReference.html#nodiscard-warn-unused-result
[6] overloadable: https://clang.llvm.org/docs/AttributeReference.html#overloadable
[7] сессии WWDC 413: https://developer.apple.com/videos/play/wwdc2012/413/
[8] документации Clang: https://clang.llvm.org/docs/ObjectiveCLiterals.html
[9] Алекс Денисов: https://twitter.com/1101_debian
[10] сделал патч: https://lowlevelbits.org/nsvalue-and-boxed-expressions/
[11] objc_boxable: https://clang.llvm.org/docs/AttributeReference.html#objc-boxable
[12] compound literal: https://en.cppreference.com/w/c/language/compound_literal
[13] nullability-аннотации: https://clang.llvm.org/docs/AttributeReference.html#nullability-attributes
[14] rdar://27062504: https://openradar.appspot.com/27062504
[15] RBBNotNil: https://gist.github.com/robb/d55b72d62d32deaee5fa
[16] @available: https://clang.llvm.org/docs/LanguageExtensions.html#objective-c-available
[17] @keypath: https://github.com/jspahrsummers/libextobjc/blob/master/extobjc/EXTKeyPathCoding.h
[18] libextobjc: https://github.com/jspahrsummers/libextobjc
[19] https://pspdfkit.com/blog/2017/even-swiftier-objective-c/: https://pspdfkit.com/blog/2017/even-swiftier-objective-c/
[20] https://medium.com/@maicki/type-inference-with-auto-type-55a38ef56372: https://medium.com/@maicki/type-inference-with-auto-type-55a38ef56372
[21] https://nshipster.com/__attribute__/: https://nshipster.com/__attribute__/
[22] https://www.bignerdranch.com/blog/bools-sharp-corners/: https://www.bignerdranch.com/blog/bools-sharp-corners/
[23] gist: https://gist.github.com/artyom-stv/d14ee77734e170b8a7413ac6b3981aae
[24] Алгебраические типы данных: https://ru.wikipedia.org/wiki/%D0%90%D0%BB%D0%B3%D0%B5%D0%B1%D1%80%D0%B0%D0%B8%D1%87%D0%B5%D1%81%D0%BA%D0%B8%D0%B9_%D1%82%D0%B8%D0%BF_%D0%B4%D0%B0%D0%BD%D0%BD%D1%8B%D1%85
[25] Источник: https://habr.com/post/431236/?utm_campaign=431236
Нажмите здесь для печати.