Emptiness 空值語義

原文: 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) OptionalArray 產(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, maxmin 都會(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. 那么, 我們就可以往 OptionalArray 類型添加方法(在這之前, 你只能給使用了遵守了協(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, AgdaIdris. 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, maxmin 里返回 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 的方法.

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末钓株,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子驾中,更是在濱河造成了極大的恐慌悠砚,老刑警劉巖留攒,帶你破解...
    沈念sama閱讀 206,723評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡阵苇,警方通過查閱死者的電腦和手機(jī)备禀,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,485評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門洲拇,熙熙樓的掌柜王于貴愁眉苦臉地迎上來奈揍,“玉大人,你說我怎么就攤上這事赋续∧泻玻” “怎么了?”我有些...
    開封第一講書人閱讀 152,998評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵纽乱,是天一觀的道長(zhǎng)蛾绎。 經(jīng)常有香客問我,道長(zhǎng)鸦列,這世上最難降的妖魔是什么租冠? 我笑而不...
    開封第一講書人閱讀 55,323評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮薯嗤,結(jié)果婚禮上顽爹,老公的妹妹穿的比我還像新娘。我一直安慰自己应民,他們只是感情好话原,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,355評(píng)論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著诲锹,像睡著了一般繁仁。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上归园,一...
    開封第一講書人閱讀 49,079評(píng)論 1 285
  • 那天黄虱,我揣著相機(jī)與錄音,去河邊找鬼庸诱。 笑死捻浦,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的桥爽。 我是一名探鬼主播朱灿,決...
    沈念sama閱讀 38,389評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼钠四!你這毒婦竟也來了盗扒?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,019評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤缀去,失蹤者是張志新(化名)和其女友劉穎侣灶,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體缕碎,經(jīng)...
    沈念sama閱讀 43,519評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡褥影,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,971評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了咏雌。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片凡怎。...
    茶點(diǎn)故事閱讀 38,100評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡校焦,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出栅贴,到底是詐尸還是另有隱情斟湃,我是刑警寧澤,帶...
    沈念sama閱讀 33,738評(píng)論 4 324
  • 正文 年R本政府宣布檐薯,位于F島的核電站凝赛,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏坛缕。R本人自食惡果不足惜墓猎,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,293評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望赚楚。 院中可真熱鬧毙沾,春花似錦、人聲如沸宠页。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,289評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)举户。三九已至烤宙,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間俭嘁,已是汗流浹背躺枕。 一陣腳步聲響...
    開封第一講書人閱讀 31,517評(píng)論 1 262
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留供填,地道東北人拐云。 一個(gè)月前我還...
    沈念sama閱讀 45,547評(píng)論 2 354
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像近她,于是被迫代替她去往敵國(guó)和親叉瘩。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,834評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容