英文原文链接
Swift 的一大优点是它能与 Objective-C 代码混编,不论是由 Objective-C 写成的库还是你的应用中的 Objective-C 代码都可以畅通的与 Swift 交互。然而,在 Swift 里可选(optional)的引用与非可选(non-optional)的引用泾渭分明,例如NSView VS. 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的指针则不可能。如果违反这些规则,编译器会发出警告。
@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 的指针。
- (nullable AAPLListItem *)itemWithName:(nonnull NSString *)name; - (NSInteger)indexOfItem:(nonnull AAPLListItem *)item;
对于属性,也可以不用加下划线,写在属性特性列表里即可。
- (nullable AAPLListItem *)itemWithName:(nonnull NSString *)name; - (NSInteger)indexOfItem:(nonnull AAPLListItem *)item;
不加下划线的形式比加下划线的要好一些,不过你仍然需要在头文件的每个类型前都加一遍注解。想要轻松一些同时让头文件更简洁,你可以使用审核区(audited region)。
审核区
为了循序渐进地采用这些新注解,你可以给 Objective-C 头文件中的一个区块加上“经过非空审核”的标记。在这个区块内部,每个普通的指针类型都会默认为非空。这能让上文的例子大大简化:
<span style="color:#ff0000;">NS_ASSUME_NONNULL_BEGIN</span><span style="color:#3e3e3e;"> @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 </span><span style="color:#ff0000;">NS_ASSUME_NONNULL_END</span><span style="color:#3e3e3e;"> // -------------- self.list.name = nil; // okay AAPLListItem *matchingItem = [self.list itemWithName:nil]; // warning!</span>
出于安全考虑,这条规则有几个例外:
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 代码加注解之前:
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 } // ... }
加了注解之后:
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。