可空性与 Objective-C
Swift 的一大优点是它能与 Objective-C 代码混编,不论是由 Objective-C 写成的库还是你的应用中的 Objective-C 代码都可以畅通的与 Swift 交互。然而,在 Swift 里可选(optional)的引用与非可选(non-optional)的引用泾渭分明,例如NSView
相对于NSView?
,在 Objective-C 里这两种类型都对应NSView *
。因为 Swift 的编译器判断不出某个NSView *
是否是可选值,因此转换为 Swift 的类型就是隐式解包可选类型NSView!
。
在之前版本的 Xcode 中,有些苹果官方的框架经过了专门的审核,来让 API 正确反映在 Swift 中是否是可选类型。为了支持在你自己的代码里也能实现这种功能,Xcode 6.3 推出了一项新的 Objective-C 语言特性:nullability annotations 。
核心:nullable 与 nonnull
这项新特性的核心是两个新的类型注解:__nullable
与__nonnull
。正如你所想,标记了__nullable
的指针的值有可能为NULL
或nil
,而__nonnull
的指针则不可能。如果违反这些规则,编译器会发出警告。
1 2 3 4 5 6 7 8 9 10 |
@interface AAPLList : NSObject <NSCoding, NSCopying> // ... - (AAPLListItem * __nullable)itemWithName:(NSString * __nonnull)name; @property (copy, readonly) NSArray * __nonnull allItems; // ... @end // -------------- [self.list itemWithName:nil]; // warning! |
几乎所有能用 C 传统的const
关键字的地方都能用__nullable
和__nonnull
,当然只能用在指针类型上。不过,一般情况下有一个更好的方法来使用这两个注解:用在方法声明里时可以不用加下划线,直接写在左括号后面,只需后面的类型是普通对象或 block 的指针。
1 2 |
- (nullable AAPLListItem *)itemWithName:(nonnull NSString *)name; - (NSInteger)indexOfItem:(nonnull AAPLListItem *)item; |
对于属性,也可以不用加下划线,写在属性特性列表里即可。
1 2 |
@property (copy, nullable) NSString *name; @property (copy, readonly, nonnull) NSArray *allItems; |
不加下划线的形式比加下划线的要好一些,不过你仍然需要在头文件的每个类型前都加一遍注解。想要轻松一些同时让头文件更简洁,你可以使用审核区(audited region)。
审核区
为了循序渐进地采用这些新注解,你可以给 Objective-C 头文件中的一个区块加上“经过非空审核”的标记。在这个区块内部,每个普通的指针类型都会默认为非空。这能让上文的例子大大简化:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
NS_ASSUME_NONNULL_BEGIN @interface AAPLList : NSObject <NSCoding, NSCopying> // ... - (nullable AAPLListItem *)itemWithName:(NSString *)name; - (NSInteger)indexOfItem:(AAPLListItem *)item; @property (copy, nullable) NSString *name; @property (copy, readonly) NSArray *allItems; // ... @end NS_ASSUME_NONNULL_END // -------------- self.list.name = nil; // okay AAPLListItem *matchingItem = [self.list itemWithName:nil]; // warning! |
出于安全考虑,这条规则有几个例外:
typedef
类型一般没有内在的可空性——它们要么为空,要么非空,根据具体的取值而定。因此,即使在审核区内,typedef
类型也不会默认为nonnull
。- 复合指针类型,如
id *
,必须显式使用注解。例如,指明一个非空的指针指向可空的对象引用,要用__nullable id * __nonnull
。 - 一个比较特殊的类型
NSError **
一般用来在方法参数中返回错误信息,因此它总是默认为可空的指针指向可空的NSError
引用。
这方面的更多信息可以参考Error Handling Programming Guide。
兼容性
如果现有的代码用到了你的 Objective-C 框架,却与你指定的非空性规则相互抵触怎么办?就这样一下子改变你的类型真的安全吗?放心,是安全的。
- 现有的编译好的代码如果用到了你的框架,仍然能正常运行,也就是说 ABI 不会变。这也意味着这些现有的代码在运行时不会捕捉到传递
nil
的错误。 - 现有的源代码如果用到了你的框架,用新的 Swift 编译器编译时,会得到不安全用法的警告。
- nonnull 不会影响编译优化。尤其是,你仍然可以在运行时检查标注为
nonnull
的参数,看它实际上是不是nil
。这对于向前兼容可能是必要的。
总体来说,基本上你应该像看待 assertion 或 exception 一样看待nullable
与nonnull
:违反这些规则是程序员的错误。尤其是,返回值是你能控制的,所以一定不要对非空的返回类型返回nil
,除非是为了向前兼容。
回到 Swift
现在我们已经给 Objective-C 的头文件加了可空性注解,来看看在 Swift 里该怎么用吧:
在给 Objective-C 代码加注解之前:
1 2 3 4 5 6 7 8 9 |
class AAPLList : NSObject, NSCoding, NSCopying { // ... func itemWithName(name: String!) -> AAPLListItem! func indexOfItem(item: AAPLListItem!) -> Int @NSCopying var name: String! { get set } @NSCopying var allItems: [AnyObject]! { get } // ... } |
加了注解之后:
1 2 3 4 5 6 7 8 9 |
class AAPLList : NSObject, NSCoding, NSCopying { // ... func itemWithName(name: String) -> AAPLListItem? func indexOfItem(item: AAPLListItem) -> Int @NSCopying var name: String? { get set } @NSCopying var allItems: [AnyObject] { get } // ... } |
Swift 的代码更清晰了。只是一个微小的改变,却能让你的框架更易于使用。
C 和 Objective-C 的可空性注解在 Xcode 6.3 及以后版本可用。查看更多信息,请参考Xcode 6.3 Release Notes。