本系列文章為個(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.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)在,我們也終于可以如愿了。