使用func和closure加工數(shù)據(jù)(二)

使用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é)果里,把FreePaid的視頻按時間排序藻懒。理解了上面的套路之后,就很簡單了视译,我們繼續(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的時候井辜,把Arraybridge 到 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ù)類型默認是不能逃離的掷邦,因此我們需要明確告知編譯器這種情況白胀。

然后,我們就可以這樣來定義用于按typelength排序的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的情況,模擬了rl相等的情況蓬衡。其余喻杈,就是我們之前提到的三點核心思想的實現(xiàn),很簡單狰晚。有了combine方法筒饰,我們就可以把之前的typeDescriptorlengthDescriptor合并起來了:

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)在忍法,我們也終于可以如愿了。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末榕吼,一起剝皮案震驚了整個濱河市饿序,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌羹蚣,老刑警劉巖嗤堰,帶你破解...
    沈念sama閱讀 211,817評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異度宦,居然都是意外死亡,警方通過查閱死者的電腦和手機告匠,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,329評論 3 385
  • 文/潘曉璐 我一進店門戈抄,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人后专,你說我怎么就攤上這事划鸽。” “怎么了戚哎?”我有些...
    開封第一講書人閱讀 157,354評論 0 348
  • 文/不壞的土叔 我叫張陵裸诽,是天一觀的道長。 經(jīng)常有香客問我型凳,道長丈冬,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,498評論 1 284
  • 正文 為了忘掉前任甘畅,我火速辦了婚禮埂蕊,結(jié)果婚禮上往弓,老公的妹妹穿的比我還像新娘。我一直安慰自己蓄氧,他們只是感情好函似,可當我...
    茶點故事閱讀 65,600評論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著喉童,像睡著了一般撇寞。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上堂氯,一...
    開封第一講書人閱讀 49,829評論 1 290
  • 那天蔑担,我揣著相機與錄音,去河邊找鬼祖灰。 笑死钟沛,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的局扶。 我是一名探鬼主播恨统,決...
    沈念sama閱讀 38,979評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼三妈!你這毒婦竟也來了畜埋?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,722評論 0 266
  • 序言:老撾萬榮一對情侶失蹤畴蒲,失蹤者是張志新(化名)和其女友劉穎悠鞍,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體模燥,經(jīng)...
    沈念sama閱讀 44,189評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡咖祭,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,519評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了蔫骂。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片么翰。...
    茶點故事閱讀 38,654評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖辽旋,靈堂內(nèi)的尸體忽然破棺而出浩嫌,到底是詐尸還是另有隱情,我是刑警寧澤补胚,帶...
    沈念sama閱讀 34,329評論 4 330
  • 正文 年R本政府宣布码耐,位于F島的核電站,受9級特大地震影響溶其,放射性物質(zhì)發(fā)生泄漏骚腥。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,940評論 3 313
  • 文/蒙蒙 一瓶逃、第九天 我趴在偏房一處隱蔽的房頂上張望桦沉。 院中可真熱鬧每瞒,春花似錦、人聲如沸纯露。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,762評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽埠褪。三九已至浓利,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間钞速,已是汗流浹背贷掖。 一陣腳步聲響...
    開封第一講書人閱讀 31,993評論 1 266
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留渴语,地道東北人苹威。 一個月前我還...
    沈念sama閱讀 46,382評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像驾凶,于是被迫代替她去往敵國和親牙甫。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 43,543評論 2 349

推薦閱讀更多精彩內(nèi)容

  • #文明之光#: 自從訂閱了吳軍老師的硅谷來信调违,剛開始我的睡眠時間變晚了窟哺,因為硅谷來信是每天零點更新。每天深夜閱讀更...
    隨思錄閱讀 321評論 0 0
  • 可能怨相識太早 歲月太長 好久不見啊 老朋友 你可過得靜好 曾無話不談 呼吸一片天 吃同樣的苦 遭受同樣的埋怨 而...
    寫手沈然閱讀 369評論 0 0