- PVSM.RU - https://www.pvsm.ru -

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

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

В компиляторе Swift по-прежнему находятся баги, которые приводят к его падению или генерации неправильного кода. У Swift нет стабильного ABI. И, что очень важно, проекты на Swift собираются слишком долго.

В связи с этим существующим проектам может быть выгоднее продолжать разработку на Objective-C. А Objective-C уже не тот, что был раньше!

В этом цикле статей мы покажем полезные возможности и улучшения Objective-C, с которыми писать код становится намного приятнее. Каждый, кто пишет на Objective-C, найдет для себя что-нибудь интересное.

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

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]. Но есть ряд ограничений, при которых нужно явно указывать тип возвращаемого значения.

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

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

Generics и 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);
}

Сниппет для 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

Раз уж мы переобъявили методы -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);

Boxed expressions

В далеком 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 literals

Еще одна полезная конструкция языка — 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
};

Сниппеты для 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

В 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