原文: Emptiness
作者: Soroush Khanlou
譯者: kemchenj
如果 Swift 里的 array 數(shù)組不能為空?
仔細(xì)想想: 如果 Swift 已經(jīng)設(shè)計(jì)了非空的數(shù)組了. 但這會(huì)讓人很煩對(duì)吧? 什么語言有非空的數(shù)組?
然而, Swift 比起 C 語言已經(jīng)修改了很多規(guī)則了. 例如, switch 里不需要 break
了, 甚至可以使用 fallthrough
來把幾個(gè) case 連接起來. 沒有了 ++
操作符, 它是那么的讓人迷惑, 多余, 并且沒了它語言會(huì)變得更好.
還有一點(diǎn) Swift 跟 C 不一樣, Swift 需要顯式地聲明可空性. Swift 讓你使用 Optional
類型, 向類型系統(tǒng)指定某個(gè)值是否可能有空. 你可以說你有一個(gè) controller, 或者可能有一個(gè) controller 也可能沒有. 類型系統(tǒng)可以在所有地方都檢查一遍, 保證這個(gè)值在被需要使用時(shí)不會(huì)為空.
Doubly Empty
當(dāng) Optional
和 Array
產(chǎn)生交匯時(shí), 你會(huì)有兩種方式去描述空值: nil 或者是空數(shù)組.
這可能會(huì)有點(diǎn)繞, 例如, 當(dāng)你檢查一個(gè)數(shù)組是否為 nil 或者為空數(shù)組的時(shí)候. 例如, 你想要更好地使用 Swift 里的 optional chaining 的時(shí)候, optionalArray?.isEmpty
卻返回了一個(gè) Optional<Bool>
, 一個(gè)本質(zhì)上很讓人迷惑的類型. 如果在 if
判斷句里使用了這一描述的話, 編譯器會(huì)拋出一個(gè)編譯錯(cuò)誤, 因?yàn)檫@是一個(gè) Optional 的布爾值.
optionalArray == []
會(huì)被編譯, 但會(huì)在數(shù)組為 nil 的時(shí)候返回 false
, 而這并不是我們想要的行為. 你可以有這么幾種方式達(dá)到目的, 但不多:
if optionalArray == nil || optionalArray == [] {
if let array = optionalArray, array.isEmpty {
if (optionalArray ?? []).isEmpty {
if optionalArray?.isEmpty != false {
if optionalArray?.isEmpty ?? false {
最簡(jiǎn)單的方法是記住不要使用 Optional 的數(shù)組. 我一直嚴(yán)格遵守著這個(gè)規(guī)則, 保證不會(huì)把不同類型的"空值"混合到一起. 對(duì)于別的"可空"類型我也是這么做的 - 字典, 字符串, 布爾值和一些別的類型. 不得不去檢查兩種類型的控制檢查是我最不想做的事情.
遵守這個(gè)規(guī)則很容易, 例如說一個(gè)類里的屬性, 但不可能在所有情況下都遵守這個(gè)規(guī)則. 例如, 從一個(gè) Optional 的實(shí)例那里獲取一個(gè)數(shù)組屬性就會(huì)成為一個(gè) Optional 的數(shù)組.
let wheels = optionalCar?.wheels // 結(jié)果是 [Wheel]?
從一個(gè)字典里面去獲取數(shù)組也是一樣.
let wheels = dictionary["wheels"] as? [Wheel]
你不得不去在每一個(gè)語句后面都加上 ?? []
.
我們剛擺脫了無法分辨 controller 和可空 controller 的困境. 獲得了簡(jiǎn)化語句, 減少錯(cuò)誤和可聲明的能力. 現(xiàn)在卻又遇上了這種窘境.
如果一個(gè)數(shù)組不能為空, 那 Optional 的數(shù)組就代表了空數(shù)組, 非 Optional 的數(shù)組則總會(huì)包含至少一個(gè)值. 就不可能同時(shí)出現(xiàn)兩種語義上的空值了, 而任何采用了別的語義的代碼都不會(huì)通過編譯.
Modeling
非空數(shù)組對(duì)于建立模型也很有用處. 告訴類型系統(tǒng)一個(gè)給定的數(shù)組永遠(yuǎn)不可能為空有時(shí)候很有用. 例如, 也許你的 User
類有許多個(gè)郵箱, 但如果 user 沒有郵箱的話則不應(yīng)該被驗(yàn)證. 可以讓類型系統(tǒng)接收這樣的描述是一件很棒的時(shí)期, 但現(xiàn)在我們做不到. 其他例子:
- 一個(gè)
Country
國(guó)家必須有至少一座City
城市. - 一張
Album
專輯必須有至少一首Song
歌. - 一棟
Building
樓必須有至少一層Floor
.
這樣的例子一大堆.
如果一個(gè)數(shù)組類型不能為空, 這些關(guān)系和約束全部都可以在類型系統(tǒng)里展現(xiàn)出來, 并且你不能刪掉數(shù)組里的最后一個(gè)元素.
Expressions 語句表述
隨著 Optional
類型的出現(xiàn), 許多表述都被簡(jiǎn)化 當(dāng)你知道一個(gè)類型永遠(yuǎn)不可能為空的時(shí)候, 你可以跳過空值檢查, 用一個(gè)更直觀的方式去操作它. 對(duì)于非空的數(shù)組也是一樣的. 現(xiàn)在, Collection
協(xié)議的方法, 例如 first
, last
, max
和 min
都會(huì)返回 Optional
, 只是為了處理數(shù)組為空的情況.
有許多的情況下數(shù)組都不會(huì)為空, 但每當(dāng)我使用諸如 first
之類的方法的時(shí)候, 我還是不得不去做防御, 僅僅只是為了告訴類型系統(tǒng)它不為空.
如果數(shù)組不可能為空的話, 這些方法都可以返回一個(gè)非空值, 使用這些語句都會(huì)變得更容易. 空數(shù)組可以通過 optional chaining 來調(diào)用這些方法, 而返回值也會(huì)是 Optional
.
Appending 插入
如果數(shù)組不可能為空, 那往非空數(shù)組里插入內(nèi)容就可以很正常地工作. 但往一個(gè)可空數(shù)組里插入值就會(huì)是一場(chǎng)災(zāi)難.
var emptiableArray = //...
emptiableArray == nil
? emptiableArray = [newItem]
: emptiableArray?.append(newItem)
這很讓人心煩, 但好消息是, 在 Swift 3.1 里, 我們可以給特定類型的泛型類型添加 extension. 那么, 我們就可以往 Optional
的 Array
類型添加方法(在這之前, 你只能給使用了遵守了協(xié)議的某個(gè)類型添加 extension)
extension Optional<Array<Element>> {
func append(_ element: Element) {
switch self {
case .some(array):
array.append(element)
case .none:
self = [element]
}
}
}
現(xiàn)在我們可以像之前那樣暢通無阻的操作了.
Without Loss Of Generality
我們?cè)龠M(jìn)一步, 如果數(shù)組的泛型參數(shù)包含了數(shù)組長(zhǎng)度呢? 例如, 給 Array<of: 4, element: String>
插入一個(gè)值的時(shí)候就會(huì)返回一個(gè) Array<of: 5, element: String
. 這個(gè)概念被稱為 dependent types, 并且在一些實(shí)驗(yàn)性的帶有更先進(jìn)的類型系統(tǒng)的語言里已經(jīng)實(shí)現(xiàn)了, 例如 Coq, Agda 和 Idris. Oisín 討論過如何在 Swift 里實(shí)現(xiàn)一樣的東西出來.
雖然這些東西非常好玩, 但也有一點(diǎn)不切實(shí)際. 你想想, 這意味著你不能在類里保存數(shù)組了, 除非你知道這個(gè)數(shù)組的長(zhǎng)度永遠(yuǎn)不會(huì)被改變. 在很多情況下, 你不可能知道編譯時(shí)會(huì)有多少個(gè)對(duì)象從 API 和數(shù)據(jù)庫(kù)里被返回
簡(jiǎn)單的鑒別 空/非空 有很明確的現(xiàn)實(shí)意義, 并且也會(huì)簡(jiǎn)化 Swift 很多內(nèi)部運(yùn)作方式.
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.
這篇文章更像是一個(gè) Idea 的嘗試. 但這也只是一個(gè)常規(guī)嘗試. 作為結(jié)尾, 我建立了一個(gè)非空數(shù)組類型. 你可以到這里看源碼, 運(yùn)作起來就像一個(gè)數(shù)組, 但不為空. 遵守 Sequence
, Collection
協(xié)議并且有 ==
和 !=
方法.
由于 Swift 的類型系統(tǒng)有一部分我沒能完全理解, 但盡管如此, 你還是可以重寫協(xié)議(例如 Collection
)里的方法(例如 first
), 然后把 Element?
修改了 Element
, Swift 會(huì)在調(diào)用時(shí)爭(zhēng)產(chǎn)工作, 并且使用更加明確的類型, Element
. 這意味著 NonEmptyArray
會(huì)在 first
, last
, max
和 min
里返回 non-optional, 雖然 Collection
里它們被定義為 Optional
, repo 里的測(cè)試有斷言來判斷這個(gè).
擁有一個(gè)絕對(duì)不為空的數(shù)組會(huì)有很多有趣的事情發(fā)生. 插入還好, 但刪除元素的方法會(huì)帶來更多問題. 我把這個(gè)方法標(biāo)記為 throws
, 但經(jīng)過更多思考之后, 這也許不是一種正確地做法. 畢竟, Swift 原生的數(shù)組刪除元素時(shí)也會(huì)產(chǎn)生問題, 只是它比起 NonEmptyArray
可以一個(gè)以上的元素. Swift 的數(shù)組會(huì)在嘗試刪除空數(shù)組的元素時(shí)調(diào)用 fatalError
, 所以也許這才是正確地做法.
我很期待可以把 NonEmptyArray
拆分成幾個(gè)提案, 看看失去 Swift 原生數(shù)組類型的語法糖是否值得, 去換取返回 non-optional 的方法.