可空性與 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。
打賞支援我翻譯更多好文章,謝謝!
打賞譯者
打賞支援我翻譯更多好文章,謝謝!
任選一種支付方式