大多数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
语言扩展,并且在Objective-C ++中可以使用Xcode 8类型推断之前(在C ++ 0X出现时使用auto
关键字)。
首先, let
添加let
和var
宏:
#define let __auto_type const #define var __auto_type
如果您过去是在指向Objective-C类的指针之后编写const
,那么这是不可接受的,但是现在隐式指定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
警告(在项目设置的“ Other Warning Flags”中添加-Wno-auto-var-id
)。
自动块类型返回值
很少有人知道编译器可以推断块返回值的类型:
let block = ^{ return @"abc"; };
非常方便。 特别是如果您使用ReactiveObjC编写反应式代码。 但是,有许多限制必须明确指定返回值的类型。
如果一个块中有多个return
,则返回不同类型的值。
let block1 = ^NSUInteger(NSUInteger value){ if (value > 0) { return value; } else {
如果该块中有一个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中。 我们希望您已经使用它们。 但是,如果没有,您可以观看WWDC会话或在此处或此处阅读。
对于我们自己,我们已经开发了一个规则,始终指定通用参数,即使它是id
( NSArray<id> *
)。 因此,很容易区分传统代码中尚未指定通用参数的代码。
借助let
和var
宏,我们希望可以在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_))
但是对于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_))
现在我们的代码看起来好多了:
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];
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];
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引入了NSNumber
, NSArray
和NSDictionary
文字以及框式表达式。 您可以在Clang文档中阅读有关文字和带框表达式的更多信息。
使用文字和框式表达式,您可以轻松地获得一个代表数字或布尔值的对象。 但是要获得包装结构的对象,您需要编写一些代码:
为某些类定义了辅助方法和属性(例如+[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 getValue:]
方法或category方法来-[NSValue getValue:]
结构。
CoreGraphics定义了自己的宏CG_BOXABLE
,并且CGPoint
, CGSize
, CGVector
和CGRect
已经支持盒装表达式。
对于其他常用结构,我们可以自己添加对框式表达式的支持:
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
代码本身:
当某些字段为零时,使用这种构造更加方便:
(CGPoint){ .y = 10 }
当然,在复合文字中,您不仅可以使用常量,还可以使用任何表达式:
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
在整个文件中添加丢失的nullable
, null_resettable
和_Nullable
。
默认情况下,所有有用的可空性编译器警告均处于启用状态。 但是,我想提出一个极端警告: -Wnullable-to-nonnull-conversion
(在项目设置的“其他警告标志”中设置)。 当具有可为空类型的变量或表达式隐式转换为非空类型时,编译器会生成此警告。
+ (NSString *)foo:(nullable NSString *)string { return string;
不幸的是,对于__auto_type
(因此是let
和var
),此警告不起作用。 通过__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 {
请注意,在撰写本文时, nullable-to-nonnull-conversion
警告并不nullable-to-nonnull-conversion
:编译器尚未了解nullable
变量,在检查了不等式之后, nil
可以解释为nonnull
。
- (NSString *)foo:(nullable NSString *)string { if (string != nil) { return string;
在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库的其余部分。
→本文的代码发布在gist中 。
结论
在本文的第一部分中,我们试图讨论主要功能和简单的语言改进,这极大地促进了Objective-C代码的编写和支持。 在下一部分中,我们将展示如何使用Swift中的枚举(它们也是案例类;它们也是代数数据类型 ,ADT)进一步提高生产率,以及在协议级别实现方法的可能性。