如何通過(guò)類型系統(tǒng)模擬OC的運(yùn)行時(shí)特性?

本系列文章為個(gè)人學(xué)習(xí)筆記:禁止轉(zhuǎn)載

從排序函數(shù)開(kāi)始

為了模擬NSSortDescriptor的實(shí)現(xiàn),我們得先從它的排序函數(shù)做起。簡(jiǎn)單來(lái)說(shuō),這就是一個(gè)接受兩個(gè)同類型的參數(shù),并且返回Bool的函數(shù),我們可以用一個(gè)typealias來(lái)表示:

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

于是,兩個(gè)比較String的descriptor可以寫(xiě)成:

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

但有時(shí),我們實(shí)際上要比較的內(nèi)容,不是T,而是T的某個(gè)屬性,例如,我們要比較上一節(jié)中Episode的長(zhǎng)度:

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

觀察這兩個(gè)例子,如果我們要抽象SortDescriptor的創(chuàng)建過(guò)程,要解決兩個(gè)問(wèn)題:

首先,對(duì)于要排序的值,不能簡(jiǎn)單的認(rèn)為就是SortDescriptor泛型參數(shù)的對(duì)象,它還有可能是這個(gè)對(duì)象的某個(gè)屬性。因此,我們應(yīng)該用一個(gè)函數(shù)來(lái)封裝獲取排序?qū)傩赃@個(gè)過(guò)程;

其次,對(duì)于排序的動(dòng)作,有可能是localizedCompare這樣的方法,也有可能是系統(tǒng)默認(rèn)的<操作符,因此,我們同樣要用一個(gè)函數(shù)來(lái)抽象這個(gè)比較的過(guò)程;

理解了這兩點(diǎn)之后,我們就可以試著為SortDescriptor,創(chuàng)建一個(gè)工廠函數(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ù),這是因?yàn)樵谖覀兎祷氐暮瘮?shù)里,使用了key以及isAscending,這兩個(gè)函數(shù)都逃離了makeDescriptor作用域,而Swift 3里,作為參數(shù)的函數(shù)類型默認(rèn)是不能逃離的,因此我們需要明確告知編譯器這種情況。

然后,我們就可以這樣來(lái)定義用于按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的實(shí)現(xiàn)有了一點(diǎn)改進(jìn)。我們使用了{(lán) 0.length }和{0.type }這樣的形式指定了要比較的屬性。這樣,當(dāng)指定的屬性和后面用于排序的方法使用的參數(shù)類型不一致的時(shí)候,編譯器就會(huì)報(bào)錯(cuò),避免了在運(yùn)行時(shí)因?yàn)轭愋蛦?wèn)題帶來(lái)的錯(cuò)誤。

有了這些descriptors,就離NSSortDescriptor的替代方案更進(jìn)一步了。我們先試一下其中一個(gè)descriptor:

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

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

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

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

通過(guò)extension Sequence,添加一個(gè)接受[SortDescriptor<T>]為參數(shù)的sorted(by:)方法;
定義一個(gè)可以把[SortDescriptor<T>]合并為一個(gè)SortDescriptor<T>的方法。這樣,就可以先合并,再調(diào)用sorted(by:)進(jìn)行排序;
哪種方法更好呢?為了盡可能使用統(tǒng)一的方式使用Swift集合類型,我們還是決定采用第二種方式。

那么,如何合并多個(gè)descriptors呢?核心思想有三條,在合并[SortDescriptor]的過(guò)程中:

如果某個(gè)descriptor可以比較出大小,那么后面的所有descriptor就都不再比較了;
只有某個(gè)descriptor的比較結(jié)果為相等時(shí),才繼續(xù)用后一個(gè)descriptor進(jìn)行比較;
如果所有的descriptor的比較結(jié)果都相等,則返回false;
我們來(lái)看代碼:

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
    }
}

在上面的代碼里,只有一個(gè)技巧,就是我們使用了rule(l, r)和rule(r, l)同時(shí)為false的情況,模擬了r和l相等的情況。其余,就是我們之前提到的三點(diǎn)核心思想的實(shí)現(xiàn),很簡(jiǎn)單。有了combine方法,我們就可以把之前的typeDescriptor和lengthDescriptor合并起來(lái)了:

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

然后,我們可以使用合并后的結(jié)果,對(duì)episodes進(jìn)行排序:

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

這樣,我們就可以得到和之前NSSortDescriptor同樣的結(jié)果了:

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

階段性總結(jié)
回顧下我們的Swift實(shí)現(xiàn),整體過(guò)程是這樣的:

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

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

其次,我們使用函數(shù)類型替代了OC中的Key-Value coding和selector,來(lái)獲取要排序的屬性,和執(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)建了一個(gè)[SortDescriptor<T>]。不同的是,我們沒(méi)有直接把這個(gè)數(shù)組傳遞給排序方法,而是把數(shù)組中所有的descriptor合并成了一個(gè)排序邏輯之后,再進(jìn)行排序:

// 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是一門(mén)強(qiáng)類型語(yǔ)言的特性,盡可能在編譯期保障代碼安全。另外,通過(guò)這種方案,我們還去掉了對(duì)要排序類型的限制,現(xiàn)在,它可以是任意一個(gè)Swift的原生類型:

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

我們之前說(shuō)過(guò),類似Episode這樣的類型,更適合用一個(gè)struct,現(xiàn)在,我們也終于可以如愿了。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容