Emptiness 空值語義

NO IMAGE
1 Star2 Stars3 Stars4 Stars5 Stars 給文章打分!
Loading...

原文: Emptiness
作者: Soroush Khanlou
譯者: kemchenj

如果 Swift 裡的 array 陣列不能為空?

仔細想想: 如果 Swift 已經設計了非空的陣列了. 但這會讓人很煩對吧? 什麼語言有非空的陣列?

然而, Swift 比起 C 語言已經修改了很多規則了. 例如, switch 裡不需要 break 了, 甚至可以使用 fallthrough 來把幾個 case 連線起來. 沒有了 操作符, 它是那麼的讓人迷惑, 多餘, 並且沒了它語言會變得更好.

還有一點 Swift 跟 C 不一樣, Swift 需要顯式地宣告可空性. Swift 讓你使用 Optional 型別, 向型別系統指定某個值是否可能有空. 你可以說你有一個 controller, 或者可能有一個 controller 也可能沒有. 型別系統可以在所有地方都檢查一遍, 保證這個值在被需要使用時不會為空.

<!–more–>

Doubly Empty

OptionalArray 產生交匯時, 你會有兩種方式去描述空值: nil 或者是空陣列.

這可能會有點繞, 例如, 當你檢查一個陣列是否為 nil 或者為空陣列的時候. 例如, 你想要更好地使用 Swift 裡的 optional chaining 的時候, optionalArray?.isEmpty 卻返回了一個 Optional<Bool>, 一個本質上很讓人迷惑的型別. 如果在 if 判斷句裡使用了這一描述的話, 編譯器會丟擲一個編譯錯誤, 因為這是一個 Optional 的布林值.

optionalArray == [] 會被編譯, 但會在陣列為 nil 的時候返回 false, 而這並不是我們想要的行為. 你可以有這麼幾種方式達到目的, 但不多:

if optionalArray == nil || optionalArray == [] {
if let array = optionalArray, array.isEmpty {
if (optionalArray ?? []).isEmpty {
if optionalArray?.isEmpty != false {
if optionalArray?.isEmpty ?? false {

最簡單的方法是記住不要使用 Optional 的陣列. 我一直嚴格遵守著這個規則, 保證不會把不同型別的”空值”混合到一起. 對於別的”可空”型別我也是這麼做的 – 字典, 字串, 布林值和一些別的型別. 不得不去檢查兩種型別的控制檢查是我最不想做的事情.

遵守這個規則很容易, 例如說一個類裡的屬性, 但不可能在所有情況下都遵守這個規則. 例如, 從一個 Optional 的例項那裡獲取一個陣列屬性就會成為一個 Optional 的陣列.

let wheels = optionalCar?.wheels // 結果是 [Wheel]?

從一個字典裡面去獲取陣列也是一樣.

let wheels = dictionary["wheels"] as? [Wheel]

你不得不去在每一個語句後面都加上 ?? [].

我們剛擺脫了無法分辨 controller 和可空 controller 的困境. 獲得了簡化語句, 減少錯誤和可宣告的能力. 現在卻又遇上了這種窘境.

如果一個陣列不能為空, 那 Optional 的陣列就代表了空陣列, 非 Optional 的陣列則總會包含至少一個值. 就不可能同時出現兩種語義上的空值了, 而任何採用了別的語義的程式碼都不會通過編譯.

Modeling

非空陣列對於建立模型也很有用處. 告訴型別系統一個給定的陣列永遠不可能為空有時候很有用. 例如, 也許你的 User 類有許多個郵箱, 但如果 user 沒有郵箱的話則不應該被驗證. 可以讓型別系統接收這樣的描述是一件很棒的時期, 但現在我們做不到. 其他例子:

  • 一個 Country 國家必須有至少一座 City 城市.

  • 一張 Album 專輯必須有至少一首 Song 歌.

  • 一棟 Building 樓必須有至少一層 Floor.

這樣的例子一大堆.

如果一個陣列型別不能為空, 這些關係和約束全部都可以在型別系統裡展現出來, 並且你不能刪掉陣列裡的最後一個元素.

Expressions 語句表述

隨著 Optional 型別的出現, 許多表述都被簡化 當你知道一個型別永遠不可能為空的時候, 你可以跳過空值檢查, 用一個更直觀的方式去操作它. 對於非空的陣列也是一樣的. 現在, Collection 協議的方法, 例如 first, last, maxmin 都會返回 Optional, 只是為了處理陣列為空的情況.

有許多的情況下陣列都不會為空, 但每當我使用諸如 first 之類的方法的時候, 我還是不得不去做防禦, 僅僅只是為了告訴型別系統它不為空.

如果陣列不可能為空的話, 這些方法都可以返回一個非空值, 使用這些語句都會變得更容易. 空陣列可以通過 optional chaining 來呼叫這些方法, 而返回值也會是 Optional.

Appending 插入

如果陣列不可能為空, 那往非空陣列裡插入內容就可以很正常地工作. 但往一個可空陣列裡插入值就會是一場災難.

var emptiableArray = //...
emptiableArray == nil
? emptiableArray = [newItem]
: emptiableArray?.append(newItem)

這很讓人心煩, 但好訊息是, 在 Swift 3.1 裡, 我們可以給特定型別的泛型型別新增 extension. 那麼, 我們就可以往 OptionalArray 型別新增方法(在這之前, 你只能給使用了遵守了協議的某個型別新增 extension)

extension Optional<Array<Element>> {
func append(_ element: Element) {
switch self {
case .some(array):
array.append(element)
case .none:
self = [element]
}
}
}

現在我們可以像之前那樣暢通無阻的操作了.

Without Loss Of Generality

我們再進一步, 如果陣列的泛型引數包含了陣列長度呢? 例如, 給 Array<of: 4, element: String> 插入一個值的時候就會返回一個 Array<of: 5, element: String. 這個概念被稱為 dependent types, 並且在一些實驗性的帶有更先進的型別系統的語言裡已經實現了, 例如 Coq, AgdaIdris. Oisín 討論過如何在 Swift 裡實現一樣的東西出來.

雖然這些東西非常好玩, 但也有一點不切實際. 你想想, 這意味著你不能在類裡儲存陣列了, 除非你知道這個陣列的長度永遠不會被改變. 在很多情況下, 你不可能知道編譯時會有多少個物件從 API 和資料庫裡被返回

簡單的鑑別 空/非空 有很明確的現實意義, 並且也會簡化 Swift 很多內部運作方式.

NonEmptyArray

This blog post is mostly a thought experiment. But it’s also a regular experiment. To that end, I built a non-empty array type. You can find it on GitHub here. It acts just like an array, but it isn’t emptiable. It conforms to Sequence, Collection, and has == and != methods.

這篇文章更像是一個 Idea 的嘗試. 但這也只是一個常規嘗試. 作為結尾, 我建立了一個非空陣列型別. 你可以到這裡看原始碼, 運作起來就像一個陣列, 但不為空. 遵守 Sequence, Collection 協議並且有 ==!= 方法.

由於 Swift 的型別系統有一部分我沒能完全理解, 但儘管如此, 你還是可以重寫協議(例如 Collection)裡的方法(例如 first), 然後把 Element? 修改了 Element, Swift 會在呼叫時爭產工作, 並且使用更加明確的型別, Element. 這意味著 NonEmptyArray 會在 first, last, maxmin 裡返回 non-optional, 雖然 Collection 裡它們被定義為 Optional, repo 裡的測試有斷言來判斷這個.

擁有一個絕對不為空的陣列會有很多有趣的事情發生. 插入還好, 但刪除元素的方法會帶來更多問題. 我把這個方法標記為 throws, 但經過更多思考之後, 這也許不是一種正確地做法. 畢竟, Swift 原生的陣列刪除元素時也會產生問題, 只是它比起 NonEmptyArray 可以一個以上的元素. Swift 的陣列會在嘗試刪除空陣列的元素時呼叫 fatalError, 所以也許這才是正確地做法.

我很期待可以把 NonEmptyArray 拆分成幾個提案, 看看失去 Swift 原生陣列型別的語法糖是否值得, 去換取返回 non-optional 的方法.

相關文章

IOS開發 最新文章