使用func和closure加工數(shù)據(jù)(二)
[TOC]
OC中水土不服的運行時特性
如果你有過Objective-C的開發(fā)經(jīng)驗璧诵,一定會對它提供的各種運行時特性印象深刻日月〈柯罚基于這些特性提供的功能更是靈活強大掩幢,可以幫助我們處理一些復(fù)雜的任務(wù)逊拍。但這一切上鞠,都是有代價的。沒錯芯丧,它們大多用起來都不直觀芍阎,如果你不去刨一下文檔,總不那么容易理解相關(guān)API的正確用法缨恒。
并且谴咸,既然這些特性是基于運行時的,因此骗露,編譯器僅可以對它們執(zhí)行非常有限的檢查岭佳。一旦你稍有疏忽,就得承擔App閃退的嚴重后果萧锉。
用OC運行時特性進行排序
我們來看個和搜索有關(guān)的例子珊随。首先,定義一個表示視頻信息的類:
final class Episode: NSObject {
var title: String
var type: String
var length: Int
override var description: String {
return title + "\t" + type + "\t" + String(length)
}
init(title: String, type: String, length: Int) {
self.title = title
self.type = type
self.length = length
}
}
其實在Swift里柿隙,這類內(nèi)容定義成Struct
更為合適叶洞,但為了演示OC的運行時特性,我們把它定義成了一個派生自NSObject
的類优俘。并且京办,通過關(guān)鍵字final
限制了它不能繼續(xù)被繼承。
Episode
有三個屬性帆焕,分別表示視頻的標題惭婿、類型和長度。然后叶雹,我們重載了description
屬性财饥,以便后面通過print
直接打印Episode
對象。
這一切很簡單折晦,然后钥星,我們定義一些數(shù)據(jù):
let episodes = [
Episode(title: "title 1", type: "Free", length: 520),
Episode(title: "title 4", type: "Paid", length: 500),
Episode(title: "title 2", type: "Free", length: 330),
Episode(title: "title 5", type: "Paid", length: 260),
Episode(title: "title 3", type: "Free", length: 240),
Episode(title: "title 6", type: "Paid", length: 390),
]
接下來,我們要先按type
排序满着,并在排序后的結(jié)果里谦炒,繼續(xù)按照length
排序,該怎么辦呢风喇?Apple在開發(fā)者文檔里介紹了一種叫做NSSortDescriptor
的用法宁改,這就是一個典型的功能強大,但是又必須要看文檔才能掌握的技能魂莫。
為了排序type
,首先还蹲,我們定義一個typeDescriptor
:
let typeDescriptor = NSSortDescriptor(
key: #keyPath(Episode.type),
ascending: true,
selector: #selector(NSString.localizedCompare(_:))
)
其中:
-
key
:表示要排序的屬性 -
ascending
:表示是否按升序排列 -
selector
:表示要進行比較的方法
其次,定義一個Array<NSDescriptor>
:
let descriptors = [ typeDescriptor ]
最后,把episodes
轉(zhuǎn)型成NSArray
,調(diào)用sortedArray(using:)
方法谜喊,把descriptors
傳遞給它:
let sortedEpisodes = (episodes as NSArray).sortedArray(using: descriptors)
這樣潭兽,就完成排序了,但我們會得到一個Array<Any>
的結(jié)果斗遏,為了查看它的內(nèi)容山卦,我們得這樣:
sortedEpisodes.forEach { print( $0 as! Episode )}
然后我們就可以再控制臺看到下面的結(jié)果了:
title 1 Free 520
title 2 Free 330
title 3 Free 240
title 4 Paid 500
title 5 Paid 260
title 6 Paid 390
此時,我們就完成了按Type
進行排序最易,接下來怒坯,我們還要在這個排序結(jié)果里,把Free
和Paid
的視頻按時間排序藻懒。理解了上面的套路之后,就很簡單了视译,我們繼續(xù)定義一個lengthDescriptor
:
let lengthDescriptor = NSSortDescriptor(
key: #keyPath(Episode.length),
ascending: true
)
這次嬉荆,我們使用系統(tǒng)默認的整數(shù)比較操作符就好了,可以不明確指定要使用的selector酷含。定義好之后鄙早,直接把它添加到之前創(chuàng)建的descriptors數(shù)組里:
let descriptors = [ typeDescriptor, lengthDescriptor ]
這樣重新執(zhí)行一次,sortedArray(using:)
方法就會返回這樣的結(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
看到了吧椅亚,現(xiàn)在限番,每一類視頻里,就是按照時長進行排序的了呀舔,這就是NSSortDescriptor
的用法弥虐。當你理解了這個過程之后,就能體會到它的功能強大媚赖,我們可以在descriptors
數(shù)組中霜瘪,包含任意多個不同的NSSortDescriptor
對象,來實現(xiàn)復(fù)雜的搜索功能惧磺。但是颖对,如果你不看文檔,Hmmmm...磨隘,估計你也很難理解它的使用方法缤底。
除了不怎么好學(xué)之外,上面的方法在Swift里還有個先天不足番捂,就是我們使用了OC的兩個運行時特性:一個是Key-Value coding
个唧,用來讀取屬性中的值,一個是selector
白嘁,用來表示排序時使用的算法坑鱼。編譯器對這些當然一無所知,只要語法上正確,就會開綠燈鲁沥。但是呼股,顯然,調(diào)試運行時錯誤要比編譯錯誤麻煩的多画恰。
那Swift的方式呢旦袋?
顯然,盡管NSSortDescriptor
的思想并不難掌握洞坑,但把它用在Swift里呀页,還是顯得有點水土不服,這主要表現(xiàn)在:
- 首先考润,從定義之處狭园,就限制了我們必須使用
class
,必須從NSObject
派生糊治。但顯然唱矛,這樣的信息在Swift更適合定義成Struct
- 其次,我們要在使用API的時候井辜,把
Array
bridge 到NSArray
绎谦,從NSArray
再bridge回來的時候,類型變成了Any
,我們還要手工找回類型信息 - 最后粥脚,KVC和selector都沒有利用編譯器提供足夠充分的類型檢查
所以窃肠,對于Swift原生類型來說,NSSortDescriptor
并不是復(fù)雜排序規(guī)則的最佳解決方案刷允。那就究竟該怎么辦呢冤留?你可能會想,Array
不是有一個接受函數(shù)參數(shù)的sorted
方法么:
episodes.sorted {
// Complex sorting code here
}
但這并不是一個好主意恃锉,相比之前NSSortDescriptor
的方式搀菩,不僅我們無法有效表達要排序的規(guī)則,而且破托,把這些規(guī)則統(tǒng)統(tǒng)塞進一個排序函數(shù)中也并不利于維護肪跋。想象一下,如果現(xiàn)在我們又要對title和length排序了該怎么辦呢土砂?
從排序函數(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
的某個屬性,例如逊彭,我們上面提到的Episode
的長度:
let lengthDescriptor: SortDescriptor<Episode> = {
$0.length < $1.length
}
觀察這兩個例子咸灿,如果我們要抽象SortDescriptor
的創(chuàng)建過程,要解決兩個問題:
- 首先侮叮,對于要排序的值避矢,不能簡單的認為就是
SortDescriptor
泛型參數(shù)的對象,它還有可能是這個對象的某個屬性囊榜。因此审胸,我們應(yīng)該用一個函數(shù)來封裝獲取排序?qū)傩赃@個過程; - 其次卸勺,對于排序的動作砂沛,有可能是
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>
的方法。這樣淹真,就可以先合并讶迁,再調(diào)用sorted(by:)
進行排序;
哪種方法更好呢核蘸?為了盡可能使用統(tǒng)一的方式使用Swift集合類型巍糯,我們還是決定采用第二種方式。
那么客扎,如何合并多個descriptors
呢祟峦?核心思想有三條,在合并[SortDescriptor]
的過程中:
- 如果某個
descriptor
可以比較出大小徙鱼,那么后面的所有descriptor
就都不再比較了宅楞; - 只有某個
descriptor
的比較結(jié)果為相等時,才繼續(xù)用后一個descriptor
進行比較袱吆; - 如果所有的
descriptor
的比較結(jié)果都相等厌衙,則返回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])
然后,我們可以使用合并后的結(jié)果壁晒,對episodes
進行排序:
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實現(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)在忍法,我們也終于可以如愿了。