如何在2018年用Objective-C编写 第一部分

大多数iOS项目部分或全部切换到Swift。 Swift是一种很棒的语言,它伴随着iOS开发的未来。 但是语言与工具包有着千丝万缕的联系,Swift工具包也有缺点。


Swift编译器仍然存在导致其崩溃或生成错误代码的错误。 Swift没有稳定的ABI。 而且,非常重要的是,Swift项目已经进行了太长时间。


在这方面,现有的项目可能会更有利可图,以继续在Objective-C上开发。 而且,Objective-C不再是以前的样子!


在本系列文章中,我们将向您展示Objective-C的有用功能和改进,这使编写代码更加轻松。 每个用Objective-C编写的人都会发现一些有趣的东西。



letvar


Objective-C不再需要显式指定变量类型:在Xcode 8中,出现了__auto_type语言扩展,并且在Objective-C ++中可以使用Xcode 8类型推断之前(在C ++ 0X出现时使用auto关键字)。


首先, let添加letvar宏:


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

如果您过去是在指向Objective-C类的指针之后编写const ,那么这是不可接受的,但是现在隐式指定const (通过let )已被视为理所当然。 将块保存到变量时,这种差异特别明显。


对于我们自己,我们开发了一个规则,使用letvar声明所有变量。 即使将变量初始化为nil


 - (nullable JMSomeResult *)doSomething { var result = (JMSomeResult *)nil; if (...) { result = ...; } return result; } 

唯一的例外是需要在每个代码分支中为变量分配值时:


 NSString *value; if (...) { if (...) { value = ...; } else { value = ...; } } else { value = ...; } 

只有这样,如果我们忘记为分支之一分配值,才会收到编译器警告。


最后:将letvar用于id类型的变量,您需要禁用auto-var-id警告(在项目设置的“ Other Warning Flags”中添加-Wno-auto-var-id )。


自动块类型返回值


很少有人知道编译器可以推断块返回值的类型:


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

  2. 如果该块中有一个return ,则返回nil


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

  3. 如果该块应返回BOOL


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


在C中(因此在Objective-C中)具有比较运算符的表达式的类型为int 。 因此,最好总是始终明确指定返回类型BOOL为规则。


泛型和for...in


在Xcode 7中,泛型(或更确切地说是轻量级的泛型)出现在Objective-C中。 我们希望您已经使用它们。 但是,如果没有,您可以观看WWDC会话在此处此处阅读。


对于我们自己,我们已经开发了一个规则,始终指定通用参数,即使它是idNSArray<id> * )。 因此,很容易区分传统代码中尚未指定通用参数的代码。


借助letvar宏,我们希望可以在for...in循环中使用它们:


 let items = (NSArray<NSString *> *)@[@"a", @"b", @"c"]; for (let item in items) { NSLog(@"%@", item); } 

但是此类代码无法编译。 极有可能在for...in不支持__auto_type ,因为for...in仅与实现NSFastEnumeration协议的集合NSFastEnumeration使用。 对于Objective-C中的协议,不支持泛型。


要解决此缺陷,请尝试使您的foreach宏。 首先想到的是:Foundation中的所有集合都具有objectEnumerator属性,并且该宏可能看起来像这样:


 #define foreach(object_, collection_) \ for (typeof([(collection_).objectEnumerator nextObject]) object_ in (collection_)) 

但是对于NSDictionaryNSMapTable协议方法将对键而不是值进行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#> } 

泛型和copy / mutableCopy copy


在Objective-C中无法键入的另一个地方是-mutableCopy-mutableCopy (以及-copyWithZone:-mutableCopyWithZone:方法,但我们不直接调用它们)。


为了避免显式强制转换,可以使用return类型重新声明方法。 例如,对于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)重新定义函数。 使用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); 

盒装表达式


早在2012年,在WWDC 413上, Apple引入了NSNumberNSArrayNSDictionary文字以及框式表达式。 您可以在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)] 

使用文字和框式表达式,您可以轻松地获得一个代表数字或布尔值的对象。 但是要获得包装结构的对象,您需要编写一些代码:


 //  `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属性),但这仍然不如盒装表达式方便!


在2015年, Alex Denisov为Clang 制作了一个补丁 ,允许您使用盒装表达式将NSValue所有结构包装NSValue


为了使我们的结构支持盒装表达式,您只需为该结构添加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:]方法或category方法来-[NSValue getValue:]结构。


CoreGraphics定义了自己的宏CG_BOXABLE ,并且CGPointCGSizeCGVectorCGRect已经支持盒装表达式。


对于其他常用结构,我们可以自己添加对框式表达式的支持:


 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; 

复合文字


另一个有用的语言构造是复合文字 。 复合文字在GCC中作为语言扩展出现,后来被添加到C11标准中。


如果早些时候,遇到了对UIEdgeInsetsMake的调用,我们只能猜测会得到什么缩进(我们必须观察UIEdgeInsetsMake函数的声明),然后使用复合文字来UIEdgeInsetsMake代码本身:


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

当然,在复合文字中,您不仅可以使用常量,还可以使用任何表达式:


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

可空性


在Xcode 6.3.2中,可空性注释出现在Objective-C中。 苹果开发人员添加了它们,以便将Objective-C API导入Swift。 但是,如果在语言中添加了一些内容,则应尝试将其放入服务中。 并且我们将说明我们如何在Objective-C项目中使用可空性以及存在哪些局限性。


要刷新您的知识,您可以观看WWDC会话


我们要做的第一件事是开始在所有.m文件中编写NS_ASSUME_NONNULL_BEGIN / NS_ASSUME_NONNULL_END 。 为了不手动执行此操作,我们直接在Xcode中修补文件模板。


我们还开始为所有私有属性和方法正确设置可空性。


如果将宏NS_ASSUME_NONNULL_BEGIN / NS_ASSUME_NONNULL_END添加到现有的.m文件中, null_resettable _Nullable在整个文件中添加丢失的nullablenull_resettable_Nullable


默认情况下,所有有用的可空性编译器警告均处于启用状态。 但是,我想提出一个极端警告: -Wnullable-to-nonnull-conversion (在项目设置的“其他警告标志”中设置)。 当具有可为空类型的变量或表达式隐式转换为非空类型时,编译器会生成此警告。


 + (NSString *)foo:(nullable NSString *)string { return string; // Implicit conversion from nullable pointer 'NSString * _Nullable' to non-nullable pointer type 'NSString * _Nonnull' } 

不幸的是,对于__auto_type (因此是letvar ),此警告不起作用。 通过__auto_type推导的类型将丢弃可空性注释。 而且,根据苹果开发人员在rdar中的评论:// 27062504 ,此行为不会改变。 实验上已经观察到,将_Nullable_Nonnull添加到__auto_type不会产生任何影响。


 - (NSString *)test:(nullable NSString *)string { let tmp = string; return tmp; //   } 

为了抑制nullable-to-nonnull-conversion我们编写了一个“强制展开”的宏。 想法取自RBBNotNil宏。 但是由于__auto_type的行为__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-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_INITIALIZERNS_UNAVAILABLENS_REQUIRES_SUPERNS_NOESCAPENS_ENUMNS_OPTIONS (或您具有相同属性的宏)宏和库@keypath宏。 我们还建议您查看libextobjc库的其余部分。



→本文的代码发布在gist中


结论


在本文的第一部分中,我们试图讨论主要功能和简单的语言改进,这极大地促进了Objective-C代码的编写和支持。 在下一部分中,我们将展示如何使用Swift中的枚举(它们也是案例类;它们也是代数数据类型 ,ADT)进一步提高生产率,以及在协议级别实现方法的可能性。

Source: https://habr.com/ru/post/zh-CN431236/


All Articles