nullable、__nullable、_Nullable 究竟有什麼區別呢?

在 Swift 中,我們會使用 ? 和 ! 去顯式聲明一個對象或者方法的參數是 optional 還是 non-optional ,而在 Objective-C中則沒有這一區分,這樣就會帶來一個問題:在 swift 與objective-c 混編時,Swift 編譯器並不知道一個 Objective-C 對象或者一個方法的參數到底是 optional 還是 non-optional ,因此這種情況下編譯器會隱式地都當成是 non-optional 來處理,這顯然是不太好的。

挖坑

爲了解決這個問題,蘋果在 Xcode 6.3 引入了一個 Objective-C 的新特性: Nullability Annotations ,這一新特性的核心是兩個新的類型修飾: __nullable 和 __nonnull 。從字面上我們可知, __nullable 表示對象可以是 NULL 或 nil,而 __nonnull 表示對象不應該爲空。當我們不遵循這一規則時,編譯器就會給出警告。在 Xcode 7 中,爲了避免與第三方庫潛在的衝突,蘋果把 __nonnull/__nullable改成 _Nonnull/_Nullable 。再加上蘋果同樣支持了沒有下劃線的寫法 nonnull/nullable ,於是就造成現在有三種寫法這樣混亂的局面。但是這三種寫法本質上都是互通的,只是放的位置不同,舉例如下:

方法返回值修飾:

- (nullableNSString*)method;
- (NSString* __nullable)method;
- (NSString* _Nullable)method;

聲明屬性的修飾:

@property(nonatomic,copy,nullable)NSString*aString;
@property(nonatomic,copy)NSString* __nullableaString;
@property(nonatomic,copy)NSString* _Nullable aString;

方法參數修飾:

- (void)methodWithString:(nullableNSString*)aString;
- (void)methodWithString:(NSString* _Nullable)aString;
- (void)methodWithString:(NSString* __nullable)aString;

而對於 雙指針類型對象 、 Block 的返回值 、 Block 的參數 等,這時候就不能用 nonnull/nullable 修飾,只能用帶下劃線的 __nonnull/__nullable 或者 _Nonnull/_Nullable :

- (void)methodWithError:(NSError* _Nullable * _Nullable)error
- (void)methodWithError:(NSError* __nullable* __null_unspecified)error;
// 以及其他的組合方式
- (void)methodWithBlock:(nullablevoid(^)())block;
// 注意上面的 nullable 用於修飾方法傳入的參數 Block 可以爲空,而不是修飾 Block 返回值;
- (void)methodWithBlock:(void(^ _Nullable)())block;
- (void)methodWithBlock:(void(^ __nullable)())block;
- (void)methodWithBlock:(nullableid__nonnull(^)(id__nullableparams))block;
// 注意上面的 nullable 用於修飾方法傳入的參數 Block 可以爲空,而 __nonnull 用於修飾 Block 返回值 id 不能爲空;
- (void)methodWithBlock:(id__nonnull(^ __nullable)(id__nullableparams))block;
- (void)methodWithBlock:(id_Nonnull (^ _Nullable)(id_Nullable params))block;
// the method accepts a nullable block that returns a nonnull value
// there are some more combinations here, you get the idea

以上基本上羅列了絕大部分的使用場景,但看完我們還是一臉懵逼啊,仍然不清楚什麼時候應該用哪個修飾符!

在看了原生 iOS SDK 裏 Foundation 和 UIKit 的頭文件以及蘋果的博文 《Nullability and Objective-C》 ,我們總結如下使用規範:

  • 對於屬性、方法返回值、方法參數的修飾,使用: nonnull/nullable ;
  • 對於 C 函數的參數、Block 的參數、Block 返回值的修飾,使用: _Nonnull/_Nullable , 建議棄用 __nonnull/__nullable 

Nonnull Audited Regions

如果需要每個屬性或每個方法都去指定 nonnull 和 nullable ,將是一件非常繁瑣的事。蘋果爲了減輕我們的工作量,專門提供了兩個宏: NS_ASSUME_NONNULL_BEGIN 和 NS_ASSUME_NONNULL_END 。在這兩個宏之間的代碼,所有簡單指針對象都被假定爲 nonnull ,因此我們只需要去指定那些 nullable 指針對象即可。如下代碼所示:

NS_ASSUME_NONNULL_BEGIN

@interfacemyClass()

@property(nonatomic,copy)NSString*aString;

- (id)methodWithString:(nullableNSString*)str;

@end

NS_ASSUME_NONNULL_END

在上面的代碼中, aString 屬性默認是 nonnull 的, methodWithString:方法的返回值也是 nonnull ,而方法的參數 str 被顯式指定爲 nullable 。

不過,爲了安全起見,蘋果還制定了以下幾條規則:

  • 通過 typedef 定義的類型的 nullability 特性通常依賴於上下文,即使是在 Audited Regions 中,也不能假定它爲 nonnull ;
  • 對於複雜的指針類型(如 id * )必須顯式去指定是 nonnull 還是 nullable。例如,指定一個指向 nullable 對象的 nonnull 指針,可以使用 __nullable id * __nonnull ;
  • 我們經常使用的 NSError ** 通常是被假定爲一個指向 nullable NSError 對象的 nullable 指針。

疑問

雖然在 Xcode 7 裏面,蘋果建議我們放棄使用 __nonnull/__nullable ,改用_Nonnull/_Nullable 來修飾對象可否爲空,但即使是在最新 ios 9.3 SDK 的 Foundation 和 UIKit 的頭文件裏我們可以看到官方原生類的方法參數仍然在用 __nonnull/__nullable 修飾。另外爲什麼已經有了 nonnull/nullable ,爲什麼還要增加 _Nonnull/_Nullable ?這到底是出於什麼考慮?蘋果在它的博文 《Nullability and Objective-C》 中也沒有具體解釋,於是 StackOverflow 上有個關於此問題的討論: Difference between nullable, __nullable and _Nullable in Objective-C 。

0
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章