Objective-C 中 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表示對象可以是 NULLnil,而 __nonnull表示對象不應該為空。當我們不遵循這一規則時,編譯器就會給出警告。在 Xcode 7 中,為了避免與第三方庫潛在的沖突,蘋果把__nonnull/__nullable改成 _Nonnull/_Nullable。再加上蘋果同樣支持了沒有下劃線的寫法 nonnull/nullable,于是就造成現在有三種寫法這樣混亂的局面。但是這三種寫法本質上都是互通的,只是放的位置不同,舉例如下:

方法返回值修飾:

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

聲明屬性的修飾:

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

方法參數修飾

- (void)methodWithString:(nullable NSString *)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:(nullable void (^)())block;
// 注意上面的 nullable 用于修飾方法傳入的參數 Block 可以為空,而不是修飾 Block 返回值;
- (void)methodWithBlock:(void (^ _Nullable)())block;
- (void)methodWithBlock:(void (^ __nullable)())block;
- (void)methodWithBlock:(nullable id __nonnull (^)(id __nullable params))block;
// 注意上面的 nullable 用于修飾方法傳入的參數 Block 可以為空,而 __nonnull 用于修飾 Block 返回值 id 不能為空;
- (void)methodWithBlock:(id __nonnull (^ __nullable)(id __nullable params))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
如果需要每個屬性或每個方法都去指定 nonnul和 nullable,將是一件非常繁瑣的事。蘋果為了減輕我們的工作量,專門提供了兩個宏:NS_ASSUME_NONNULL_BEGINNS_ASSUME_NONNULL_END。在這兩個宏之間的代碼,所有簡單指針對象都被假定為 nonnull,因此我們只需要去指定那些 nullable
指針對象即可。如下代碼所示:

NS_ASSUME_NONNULL_BEGIN
@interface myClass ()
@property (nonatomic, copy) NSString *aString;
- (id)methodWithString:(nullable NSString *)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

參考文獻

Objective-C 中 nullable、__nullable、_Nullable 的區別

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容