如何通過類型系統(tǒng)模擬OC的運行時特性?

本系列文章為個人學習筆記:禁止轉載

從排序函數(shù)開始

為了模擬NSSortDescriptor的實現(xiàn)歪赢,我們得先從它的排序函數(shù)做起化戳。簡單來說,這就是一個接受兩個同類型的參數(shù)埋凯,并且返回Bool的函數(shù)点楼,我們可以用一個typealias來表示:

typealias SortDescriptor<T> = (T, T) -> Bool

于是,兩個比較String的descriptor可以寫成:

let stringDescriptor: SortDescriptor<String> = {
    $0.localizedCompare($1) == .orderedAscending
}

但有時白对,我們實際上要比較的內(nèi)容掠廓,不是T,而是T的某個屬性甩恼,例如蟀瞧,我們要比較上一節(jié)中Episode的長度:

let lengthDescriptor: SortDescriptor<Episode> = { 
    $0.length < $1.length 
}

觀察這兩個例子沉颂,如果我們要抽象SortDescriptor的創(chuàng)建過程,要解決兩個問題:

首先悦污,對于要排序的值铸屉,不能簡單的認為就是SortDescriptor泛型參數(shù)的對象,它還有可能是這個對象的某個屬性切端。因此彻坛,我們應該用一個函數(shù)來封裝獲取排序屬性這個過程;

其次踏枣,對于排序的動作昌屉,有可能是localizedCompare這樣的方法,也有可能是系統(tǒng)默認的<操作符椰于,因此怠益,我們同樣要用一個函數(shù)來抽象這個比較的過程;

理解了這兩點之后瘾婿,我們就可以試著為SortDescriptor蜻牢,創(chuàng)建一個工廠函數(shù)了:

func makeDescriptor<Key, Value>(
    key: @escaping (Key) -> Value,
    _ isAscending: @escaping (Value, Value) -> Bool
) -> SortDescriptor<Key> {

        return { isAscending(key($0), key($1)) }
}

在上面的代碼里,我們使用@escaping修飾了用于獲取Value以及排序的函數(shù)參數(shù)偏陪,這是因為在我們返回的函數(shù)里抢呆,使用了key以及isAscending,這兩個函數(shù)都逃離了makeDescriptor作用域笛谦,而Swift 3里抱虐,作為參數(shù)的函數(shù)類型默認是不能逃離的,因此我們需要明確告知編譯器這種情況饥脑。

然后恳邀,我們就可以這樣來定義用于按type和length排序的descriptor:

let lengthDescriptor: SortDescriptor<Episode> =
    makeDescriptor(key: { $0.length }, <)

let typeDescriptor: SortDescriptor<Episode> =
    makeDescriptor(key: { $0.type }, {
        $0.localizedCompare($1) == .orderedAscending
    })

在上面這段代碼里,相比NSSortDescriptor的版本灶轰,Swift的實現(xiàn)有了一點改進谣沸。我們使用了{ 0.length }和{0.type }這樣的形式指定了要比較的屬性。這樣笋颤,當指定的屬性和后面用于排序的方法使用的參數(shù)類型不一致的時候乳附,編譯器就會報錯,避免了在運行時因為類型問題帶來的錯誤伴澄。

有了這些descriptors赋除,就離NSSortDescriptor的替代方案更進一步了。我們先試一下其中一個descriptor:

episodes.sorted(by: typeDescriptor)
    .forEach { print($0) }

就可以在控制臺看到已經(jīng)按type進行排序了:

title 1 Free    520
title 2 Free    330
title 3 Free    240
title 4 Paid    500
title 5 Paid    260
title 6 Paid    390

合并多個排序條件
接下來非凌,我們要繼續(xù)模擬通過一個數(shù)組來定義多個排序條件的功能举农。怎么做呢?我們有兩種選擇:

通過extension Sequence敞嗡,添加一個接受[SortDescriptor<T>]為參數(shù)的sorted(by:)方法颁糟;
定義一個可以把[SortDescriptor<T>]合并為一個SortDescriptor<T>的方法祭犯。這樣,就可以先合并滚停,再調用sorted(by:)進行排序沃粗;
哪種方法更好呢?為了盡可能使用統(tǒng)一的方式使用Swift集合類型键畴,我們還是決定采用第二種方式最盅。

那么,如何合并多個descriptors呢起惕?核心思想有三條涡贱,在合并[SortDescriptor]的過程中:

如果某個descriptor可以比較出大小,那么后面的所有descriptor就都不再比較了惹想;
只有某個descriptor的比較結果為相等時问词,才繼續(xù)用后一個descriptor進行比較;
如果所有的descriptor的比較結果都相等嘀粱,則返回false激挪;
我們來看代碼:

func combine<T>(rules: [SortDescriptor<T>]) -> SortDescriptor<T> {
    return { l, r in
        for rule in rules {
            if rule(l, r) {
                return true
            }
            
            if rule(r, l) {
                return false
            }
        }
        
        return false
    }
}

在上面的代碼里,只有一個技巧锋叨,就是我們使用了rule(l, r)和rule(r, l)同時為false的情況垄分,模擬了r和l相等的情況。其余娃磺,就是我們之前提到的三點核心思想的實現(xiàn)薄湿,很簡單。有了combine方法偷卧,我們就可以把之前的typeDescriptor和lengthDescriptor合并起來了:

let mixDescriptor = combine(rules: 
    [typeDescriptor, lengthDescriptor])

然后豺瘤,我們可以使用合并后的結果,對episodes進行排序:

episodes.sorted(by: mixDescriptor)
    .forEach { print($0) }

這樣听诸,我們就可以得到和之前NSSortDescriptor同樣的結果了:

title 3 Free    240
title 2 Free    330
title 1 Free    520
title 5 Paid    260
title 6 Paid    390
title 4 Paid    500

階段性總結
回顧下我們的Swift實現(xiàn)坐求,整體過程是這樣的:

首先,在Swift里蛇更,我們使用函數(shù)類型替代了OC中的NSSortDescriptor類瞻赶,表示了一個排序規(guī)則:

typealias SortDescriptor<T> = (T, T) -> Bool

其次赛糟,我們使用函數(shù)類型替代了OC中的Key-Value coding和selector派任,來獲取要排序的屬性,和執(zhí)行排序的selector:

func makeDescriptor<Key, Value>(
    key: @escaping (Key) -> Value,
    _ isAscending: @escaping (Value, Value) -> Bool
) -> SortDescriptor<Key> {

        return { isAscending(key($0), key($1)) }
}

第三璧南,我們用類似的方式掌逛,創(chuàng)建了一個[SortDescriptor<T>]。不同的是司倚,我們沒有直接把這個數(shù)組傳遞給排序方法豆混,而是把數(shù)組中所有的descriptor合并成了一個排序邏輯之后篓像,再進行排序:

// 1. Create descriptors
let lengthDescriptor: SortDescriptor<Episode> =
    makeDescriptor(key: { $0.length }, >)

let typeDescriptor: SortDescriptor<Episode> =
    makeDescriptor(key: { $0.type }, {
        $0.localizedCompare($1) == .orderedAscending
    })

// 2. Combine descriptor array
let mixDescriptor = combine(rules: 
    [typeDescriptor, lengthDescriptor])

// 3. Sort
episodes.sorted(by: mixDescriptor)

這樣,我們不僅保留了NSSortDescriptor的編程思想皿伺,也充分利用了Swift是一門強類型語言的特性员辩,盡可能在編譯期保障代碼安全。另外鸵鸥,通過這種方案奠滑,我們還去掉了對要排序類型的限制,現(xiàn)在妒穴,它可以是任意一個Swift的原生類型:

struct Episode: CustomStringConvertible {
    // The same as before
}

我們之前說過宋税,類似Episode這樣的類型,更適合用一個struct讼油,現(xiàn)在杰赛,我們也終于可以如愿了。

?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末矮台,一起剝皮案震驚了整個濱河市乏屯,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌瘦赫,老刑警劉巖瓶珊,帶你破解...
    沈念sama閱讀 218,122評論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異耸彪,居然都是意外死亡伞芹,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,070評論 3 395
  • 文/潘曉璐 我一進店門蝉娜,熙熙樓的掌柜王于貴愁眉苦臉地迎上來唱较,“玉大人,你說我怎么就攤上這事召川∧匣海” “怎么了?”我有些...
    開封第一講書人閱讀 164,491評論 0 354
  • 文/不壞的土叔 我叫張陵荧呐,是天一觀的道長篮绰。 經(jīng)常有香客問我少办,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,636評論 1 293
  • 正文 為了忘掉前任梳码,我火速辦了婚禮构眯,結果婚禮上糜值,老公的妹妹穿的比我還像新娘个榕。我一直安慰自己,他們只是感情好概耻,可當我...
    茶點故事閱讀 67,676評論 6 392
  • 文/花漫 我一把揭開白布使套。 她就那樣靜靜地躺著罐呼,像睡著了一般。 火紅的嫁衣襯著肌膚如雪侦高。 梳的紋絲不亂的頭發(fā)上嫉柴,一...
    開封第一講書人閱讀 51,541評論 1 305
  • 那天,我揣著相機與錄音奉呛,去河邊找鬼差凹。 笑死,一個胖子當著我的面吹牛侧馅,可吹牛的內(nèi)容都是我干的危尿。 我是一名探鬼主播,決...
    沈念sama閱讀 40,292評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼馁痴,長吁一口氣:“原來是場噩夢啊……” “哼谊娇!你這毒婦竟也來了?” 一聲冷哼從身側響起罗晕,我...
    開封第一講書人閱讀 39,211評論 0 276
  • 序言:老撾萬榮一對情侶失蹤济欢,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后小渊,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體法褥,經(jīng)...
    沈念sama閱讀 45,655評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,846評論 3 336
  • 正文 我和宋清朗相戀三年酬屉,在試婚紗的時候發(fā)現(xiàn)自己被綠了半等。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,965評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡呐萨,死狀恐怖杀饵,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情谬擦,我是刑警寧澤切距,帶...
    沈念sama閱讀 35,684評論 5 347
  • 正文 年R本政府宣布,位于F島的核電站惨远,受9級特大地震影響谜悟,放射性物質發(fā)生泄漏。R本人自食惡果不足惜北秽,卻給世界環(huán)境...
    茶點故事閱讀 41,295評論 3 329
  • 文/蒙蒙 一葡幸、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧羡儿,春花似錦礼患、人聲如沸是钥。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,894評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至虏冻,卻和暖如春肤粱,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背厨相。 一陣腳步聲響...
    開封第一講書人閱讀 33,012評論 1 269
  • 我被黑心中介騙來泰國打工领曼, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人蛮穿。 一個月前我還...
    沈念sama閱讀 48,126評論 3 370
  • 正文 我出身青樓庶骄,卻偏偏與公主長得像,于是被迫代替她去往敵國和親践磅。 傳聞我的和親對象是個殘疾皇子单刁,可洞房花燭夜當晚...
    茶點故事閱讀 44,914評論 2 355

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