本系列文章為個人學習筆記:禁止轉載
從排序函數(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.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)在杰赛,我們也終于可以如愿了。