使用func和closure加工數(shù)據(jù)(二)
[TOC]
OC中水土不服的運(yùn)行時(shí)特性
如果你有過Objective-C的開發(fā)經(jīng)驗(yàn),一定會(huì)對(duì)它提供的各種運(yùn)行時(shí)特性印象深刻?;谶@些特性提供的功能更是靈活強(qiáng)大,可以幫助我們處理一些復(fù)雜的任務(wù)。但這一切,都是有代價(jià)的。沒錯(cuò),它們大多用起來(lái)都不直觀,如果你不去刨一下文檔,總不那么容易理解相關(guān)API的正確用法。
并且,既然這些特性是基于運(yùn)行時(shí)的,因此,編譯器僅可以對(duì)它們執(zhí)行非常有限的檢查。一旦你稍有疏忽,就得承擔(dān)App閃退的嚴(yán)重后果。
用OC運(yùn)行時(shí)特性進(jìn)行排序
我們來(lái)看個(gè)和搜索有關(guān)的例子。首先,定義一個(gè)表示視頻信息的類:
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
}
}
其實(shí)在Swift里,這類內(nèi)容定義成Struct更為合適,但為了演示OC的運(yùn)行時(shí)特性,我們把它定義成了一個(gè)派生自NSObject的類。并且,通過關(guān)鍵字final限制了它不能繼續(xù)被繼承。
Episode有三個(gè)屬性,分別表示視頻的標(biāo)題、類型和長(zhǎng)度。然后,我們重載了description屬性,以便后面通過print直接打印Episode對(duì)象。
這一切很簡(jiǎn)單,然后,我們定義一些數(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),
]
接下來(lái),我們要先按type排序,并在排序后的結(jié)果里,繼續(xù)按照length排序,該怎么辦呢?Apple在開發(fā)者文檔里介紹了一種叫做NSSortDescriptor的用法,這就是一個(gè)典型的功能強(qiáng)大,但是又必須要看文檔才能掌握的技能。
為了排序type ,首先,我們定義一個(gè)typeDescriptor:
let typeDescriptor = NSSortDescriptor(
key: #keyPath(Episode.type),
ascending: true,
selector: #selector(NSString.localizedCompare(_:))
)
其中:
-
key:表示要排序的屬性 -
ascending:表示是否按升序排列 -
selector:表示要進(jìn)行比較的方法
其次,定義一個(gè)Array<NSDescriptor>:
let descriptors = [ typeDescriptor ]
最后,把episodes轉(zhuǎn)型成NSArray,調(diào)用sortedArray(using:)方法,把descriptors傳遞給它:
let sortedEpisodes = (episodes as NSArray).sortedArray(using: descriptors)
這樣,就完成排序了,但我們會(huì)得到一個(gè)Array<Any>的結(jié)果,為了查看它的內(nèi)容,我們得這樣:
sortedEpisodes.forEach { print( $0 as! Episode )}
然后我們就可以再控制臺(tái)看到下面的結(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
此時(shí),我們就完成了按Type進(jìn)行排序,接下來(lái),我們還要在這個(gè)排序結(jié)果里,把Free和Paid的視頻按時(shí)間排序。理解了上面的套路之后,就很簡(jiǎn)單了,我們繼續(xù)定義一個(gè)lengthDescriptor:
let lengthDescriptor = NSSortDescriptor(
key: #keyPath(Episode.length),
ascending: true
)
這次,我們使用系統(tǒng)默認(rèn)的整數(shù)比較操作符就好了,可以不明確指定要使用的selector。定義好之后,直接把它添加到之前創(chuàng)建的descriptors數(shù)組里:
let descriptors = [ typeDescriptor, lengthDescriptor ]
這樣重新執(zhí)行一次,sortedArray(using:)方法就會(huì)返回這樣的結(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)在,每一類視頻里,就是按照時(shí)長(zhǎng)進(jìn)行排序的了,這就是NSSortDescriptor的用法。當(dāng)你理解了這個(gè)過程之后,就能體會(huì)到它的功能強(qiáng)大,我們可以在descriptors數(shù)組中,包含任意多個(gè)不同的NSSortDescriptor對(duì)象,來(lái)實(shí)現(xiàn)復(fù)雜的搜索功能。但是,如果你不看文檔,Hmmmm...,估計(jì)你也很難理解它的使用方法。
除了不怎么好學(xué)之外,上面的方法在Swift里還有個(gè)先天不足,就是我們使用了OC的兩個(gè)運(yùn)行時(shí)特性:一個(gè)是Key-Value coding,用來(lái)讀取屬性中的值,一個(gè)是selector,用來(lái)表示排序時(shí)使用的算法。編譯器對(duì)這些當(dāng)然一無(wú)所知,只要語(yǔ)法上正確,就會(huì)開綠燈。但是,顯然,調(diào)試運(yùn)行時(shí)錯(cuò)誤要比編譯錯(cuò)誤麻煩的多。
那Swift的方式呢?
顯然,盡管NSSortDescriptor的思想并不難掌握,但把它用在Swift里,還是顯得有點(diǎn)水土不服,這主要表現(xiàn)在:
- 首先,從定義之處,就限制了我們必須使用
class,必須從NSObject派生。但顯然,這樣的信息在Swift更適合定義成Struct - 其次,我們要在使用API的時(shí)候,把
Arraybridge 到NSArray,從NSArray再bridge回來(lái)的時(shí)候,類型變成了Any,我們還要手工找回類型信息 - 最后,KVC和selector都沒有利用編譯器提供足夠充分的類型檢查
所以,對(duì)于Swift原生類型來(lái)說,NSSortDescriptor并不是復(fù)雜排序規(guī)則的最佳解決方案。那就究竟該怎么辦呢?你可能會(huì)想,Array不是有一個(gè)接受函數(shù)參數(shù)的sorted方法么:
episodes.sorted {
// Complex sorting code here
}
但這并不是一個(gè)好主意,相比之前NSSortDescriptor的方式,不僅我們無(wú)法有效表達(dá)要排序的規(guī)則,而且,把這些規(guī)則統(tǒng)統(tǒng)塞進(jìn)一個(gè)排序函數(shù)中也并不利于維護(hù)。想象一下,如果現(xiàn)在我們又要對(duì)title和length排序了該怎么辦呢?
從排序函數(shù)開始
為了模擬NSSortDescriptor的實(shí)現(xiàn),我們得先從它的排序函數(shù)做起。簡(jiǎn)單來(lái)說,這就是一個(gè)接受兩個(gè)同類型的參數(shù),并且返回Bool的函數(shù),我們可以用一個(gè)typealias來(lái)表示:
typealias SortDescriptor<T> = (T,T) -> Bool
于是,兩個(gè)比較String的descriptor可以寫成:
let stringDescriptor: SortDescriptor<String> = {
$0.localizedCompare($1) == .orderedAscending
}
但有時(shí),我們實(shí)際上要比較的內(nèi)容,不是T,而是T的某個(gè)屬性,例如,我們上面提到的Episode的長(zhǎng)度:
let lengthDescriptor: SortDescriptor<Episode> = {
$0.length < $1.length
}
觀察這兩個(gè)例子,如果我們要抽象SortDescriptor的創(chuàng)建過程,要解決兩個(gè)問題:
- 首先,對(duì)于要排序的值,不能簡(jiǎn)單的認(rèn)為就是
SortDescriptor泛型參數(shù)的對(duì)象,它還有可能是這個(gè)對(duì)象的某個(gè)屬性。因此,我們應(yīng)該用一個(gè)函數(shù)來(lái)封裝獲取排序?qū)傩赃@個(gè)過程; - 其次,對(duì)于排序的動(dòng)作,有可能是
localizedCompare這樣的方法,也有可能是系統(tǒng)默認(rèn)的<操作符,因此,我們同樣要用一個(gè)函數(shù)來(lái)抽象這個(gè)比較的過程;
理解了這兩點(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)。我們使用了{ $0.length }和{ $0.type }`這樣的形式指定了要比較的屬性。這樣,當(dāng)指定的屬性和后面用于排序的方法使用的參數(shù)類型不一致的時(shí)候,編譯器就會(huì)報(bào)錯(cuò),避免了在運(yùn)行時(shí)因?yà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ù)模擬通過一個(gè)數(shù)組來(lái)定義多個(gè)排序條件的功能。怎么做呢?我們有兩種選擇:
- 通過
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]的過程中:
- 如果某個(gè)
descriptor可以比較出大小,那么后面的所有descriptor就都不再比較了; - 只有某個(gè)
descriptor的比較結(jié)果為相等時(shí),才繼續(xù)用后一個(gè)descriptor進(jìn)行比較; - 如果所有的
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
}
}
在上面的代碼里,只有一個(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),整體過程是這樣的:
首先,在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>]。不同的是,我們沒有直接把這個(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是一門強(qiáng)類型語(yǔ)言的特性,盡可能在編譯期保障代碼安全。另外,通過這種方案,我們還去掉了對(duì)要排序類型的限制,現(xiàn)在,它可以是任意一個(gè)Swift的原生類型:
struct Episode: CustomStringConvertible {
// The same as before
}
我們之前說過,類似Episode這樣的類型,更適合用一個(gè)struct,現(xiàn)在,我們也終于可以如愿了。