Главная » Хабрахабр » Как писать на Objective-C в 2018 году. Часть 1

Как писать на Objective-C в 2018 году. Часть 1

Swift — замечательный язык, и за ним будущее разработки под iOS. Большинство iOS-проектов частично или полностью переходят на Swift. Но язык нераздельно связан с инструментарием, а в инструментарии Swift есть недостатки.

У Swift нет стабильного ABI. В компиляторе Swift по-прежнему находятся баги, которые приводят к его падению или генерации неправильного кода. И, что очень важно, проекты на 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) стало само собой разумеющимся.

Даже когда переменная инициализируется значением nil: Для себя мы выработали правило использовать let и var для объявления всех переменных.

- (nullable JMSomeResult *)doSomething 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]; }
};

  1. Если в блоке есть оператор return, возвращающий nil.

let block1 = ^NSString * _Nullable(){ return nil;
}; let block2 = ^NSString * _Nullable(BOOL flag) { if (flag) { return @"abc"; } else { return nil; }
};

  1. Если блок должен возвращать BOOL.

let predicate = ^BOOL(NSInteger lhs, NSInteger rhs){ return lhs > rhs;
};

Поэтому лучше взять за правило всегда явно указывать возвращаемый тип BOOL. Выражения с оператором сравнения в языке C (и, следовательно, в Objective-C) имеют тип int.

Generics и for...in

Надеемся, что вы их уже используете. В Xcode 7 в Objective-C появились generics (точнее, lightweight generics). Но если нет, то можно посмотреть сессию WWDC или прочитать здесь или здесь.

Таким образом можно легко отличить legacy-код, в котором generic-параметры еще не указаны. Мы для себя выработали правило всегда указывать generic-параметры, даже если это id (NSArray<id> *).

Имея макросы 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.

Первое, что приходит в голову: у всех коллекций в Foundation есть свойство objectEnumerator, и макрос мог бы выглядеть так: Чтобы исправить этот недостаток, попробуем сделать свой макрос foreach.

#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);
}

Сниппет для Xcode

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

Для этого в Clang есть атрибут warn_unused_result. Раз уж мы переобъявили методы -copy и -mutableCopy, было бы неплохо гарантировать, что результат вызова этих методов будет использован.

#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

C помощью атрибута overloadable можно создавать функции с одинаковым названием, но с разными типами аргументов или с их разным количеством. Немногие знают, что Clang позволяет переопределять функции в языке C (а следовательно, и в Objective-C).

Переопределяемые функции не могут отличаться только лишь типом возвращаемого значения.

#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

Подробно о литералах и boxed expressions можно прочитать в документации Clang. В далеком 2012 году в сессии WWDC 413 Apple представила литералы для NSNumber, NSArray и NSDictionary, а также boxed expressions.

// Литералы
@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 literals появились еще в GCC в виде расширения языка, а позже были добавлены в стандарт C11. Еще одна полезная конструкция языка — compound literal.

Если раньше, встретив вызов 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
};

Сниппеты для Xcode

(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

3. В Xcode 6. Разработчики Apple добавили их для импортирования Objective-C API в Swift. 2 в Objective-C появились nullability-аннотации. И мы расскажем, как используем nullability в Objective-C проекте и какие есть ограничения. Но если что-то добавлено в язык, то надо постараться поставить это себе на службу.

Чтобы освежить знания, можно посмотреть сессию WWDC.

Чтобы не делать этого руками, мы патчим шаблоны файлов прямо в Xcode. Первое, что мы сделали, — это начали писать макросы NS_ASSUME_NONNULL_BEGIN / NS_ASSUME_NONNULL_END во всех .m-файлах.

Мы стали также правильно расставлять nullability для всех приватных свойств и методов.

Если мы добавляем макросы NS_ASSUME_NONNULL_BEGIN / NS_ASSUME_NONNULL_END в уже существующий .m-файл, то сразу дописываем недостающие nullable, null_resettable и _Nullable во всем файле.

Но есть одно экстремальное предупреждение, которое хотелось бы включить: -Wnullable-to-nonnull-conversion (задается в "Other Warning Flags" в настройках проекта). Все полезные предупреждения компилятора, связанные с nullability, включены по умолчанию. Компилятор выдает это предупреждение, когда переменная или выражение с 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, отбрасывается nullability-аннотация. К сожалению, для __auto_type (а следовательно, и let и var) это предупреждение не срабатывает. Экспериментально замечено, что добавление _Nullable или _Nonnull к __auto_type ни на что не влияет. И, судя по комментарию разработчика Apple в rdar://27062504, это поведение уже не изменится.

- (NSString *)test:(nullable NSString *)string { let tmp = string; return tmp; // Нет предупреждения
}

Идея взята из макроса RBBNotNil. Для подавления предупреждения nullable-to-nonnull-conversion мы написали макрос, который делает "force unwrap". Но за счет поведения __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 @""; }
}

Полезные ссылки

Советуем также посмотреть остальные возможности библиотеки libextobjc. Есть также ряд более известных макросов и ключевых слов, которые хотелось бы упомянуть: ключевое слово @available, макросы NS_DESIGNATED_INITIALIZER, NS_UNAVAILABLE, NS_REQUIRES_SUPER, NS_NOESCAPE, NS_ENUM, NS_OPTIONS (или свои макросы для тех же атрибутов) и макрос @keypath из библиотеки libextobjc.

Что еще почитать

→ Код для статьи выложен в gist.

Заключение

В следующей части мы покажем, как можно еще увеличить свою продуктивность с помощью enum'ов как в Swift (они же Case-классы; они же Алгебраические типы данных, ADT) и возможности реализации методов на уровне протокола. В первой части статьи мы постарались рассказать об основных возможностях и простых улучшениях языка, которые существенно облегчают написание и поддержку Objective-C кода.


Оставить комментарий

Ваш email нигде не будет показан
Обязательные для заполнения поля помечены *

*

x

Ещё Hi-Tech Интересное!

[Перевод] Интервью с Дэвидом Гобелем

Дэвид любезно согласился дать LEAF очень интересное интервью. Дэвид Гобель – изобретатель, филантроп, футурист и ярый сторонник технологий омоложения; вместе с Обри де Греем он известен как один из основателей Methuselah Foundation и как автор концепции Longevity Escape Velocity (LEV), ...

10 долларов на хостинг: 20 лет назад и сегодня

Всё кругом дорожает, а технологии дешевеют. Когда-то компьютер или мобильный телефон могли позволить себе единицы, сейчас эти устройства есть в каждой российской семье. Цена мегабайта за последние 20 лет упала в несколько тысяч раз. Ещё один пример — хостинг. В ...