Nullability 和 Objective-C

英文原文链接

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。

你可能感兴趣的:(Nullability 和 Objective-C)