函數(shù)的靈活性

學習目的:?Swift?如何將函數(shù)作為參數(shù)使用候齿,并且將函數(shù)當作數(shù)據(jù),以完全類型安全的方式復制同樣的OC功能

例子:Objective-C?&?Swift?的排序方式

1. ”素材“:

@objcMembers ?(@objcMembers闺属,這樣它的所有成員都將在?Objective-C?中可見)

final class?Person: NSObject {

let?first: String

let?last: String

let?yearOfBirth: Int

init(first: String, last: String, yearOfBirth: Int) {

self.first =?first

self.last = last?self.yearOfBirth = yearOfBirth?// super.init()?在這里被隱式調(diào)用

}?}

接下來我們定義一個數(shù)組慌盯,其中包含了不同名字和出生年份的人:

let?people = [

Person(first:?"Emily", last:?"Young", yearOfBirth: 2002),?Person(first:?"David", last:?"Gray", yearOfBirth: 1991),?Person(first:?"Robert", last:?"Barnes", yearOfBirth: 1985),Person(first:?"Ava", last:?"Barnes", yearOfBirth: 2000),?Person(first:?"Joanne", last:?"Miller", yearOfBirth: 1994),?Person(first:?"Ava", last:?"Barnes", yearOfBirth: 1998),

]

2.排序規(guī)則:?先按照姓(last name)排序,再按照名(first name)排序掂器,最后是出生年(yearOfBirth)亚皂。

Objective-C:運行時的工作方式;selector国瓮;一個?NSSortDescriptor?數(shù)組來定義如何排序灭必;

(作為一門動態(tài)編程語言,Objective-C 會盡可能的將編譯和鏈接時要做的事情推遲到運行時巍膘。只要有可能,Objective-C 總是使用動態(tài) 的方式來解決問題厂财。)

方法:?使用?NSSortDescriptor?對象來描述如何排序?qū)ο螅ㄟ^ 它可以表達出各個排序標準?(使用?localizedStandardCompare?來進行遵循區(qū)域設置的排序): (localizedStandardCompare 字符串比較 不區(qū)分大小寫)

ascending: true 生序峡懈, false 降序


let?lastDescriptor = NSSortDescriptor(key:?#keyPath(Person.last),?ascending:?true,

selector:?#selector(NSString.localizedStandardCompare(_:)))

let?firstDescriptor = NSSortDescriptor(key:?#keyPath(Person.first),?ascending:?true,

selector:?#selector(NSString.localizedStandardCompare(_:)))

let?yearDescriptor = NSSortDescriptor(key:?#keyPath(Person.yearOfBirth),?ascending:?true)

使用?NSArray?的?sortedArray(using:)?方法璃饱,對數(shù)組進行排序

let?descriptors = [lastDescriptor,?firstDescriptor, yearDescriptor]?(people?as?NSArray).sortedArray(using: descriptors)

/*

[Ava Barnes (1998), Ava Barnes (2000), Robert Barnes (1985),

David Gray (1991), Joanne Miller (1994), Emily Young (2002)]

?*/

排序描述符用到了Objective-C?的兩個運行時特性:首先,key?是?Objective-C?的鍵路徑肪康,它其 實是一個包含屬性名字的鏈表荚恶。不要把它和?Swift 4?引入的原生的?(強類型的)?鍵路徑搞混。我 們會在稍后再對它進行更多討論磷支。

其次是鍵值編程(key-value-coding)谒撼,它可以在運行時通過鍵查找一個對象上的對應值。?selector?參數(shù)接受一個?selector (實際上也是一個用來描述方法名字的字符串)雾狈,在運行時廓潜,這 個?selector?將被用來查找比較函數(shù),當對兩個對象進行比較時善榛,這個函數(shù)將使用指定鍵對應的 值進行比較辩蛋。

這是運行時編程的一個很酷的用例,排序描述符的數(shù)組可以在運行時構(gòu)建移盆,這一點在實現(xiàn)比如 用戶點擊某一列時按照該列進行排序這種需求時會特別有用悼院。

我們要怎么用?Swift?的?sort?來復制這個功能呢?

Swift:?將函數(shù)作為參數(shù)使用,并且將函數(shù)當作數(shù)據(jù)

1: 復制部分功能簡單

var?strings = ["Hello",?"hallo",?"Hallo",?"hello"]

strings.sort { $0.localizedStandardCompare($1) == .orderedAscending}?strings?// ["hallo", "Hallo", "hello", "Hello"]

如果只是想用對象的某一個屬性進行排序的話咒循,也非常簡單:

people.sorted { $0.yearOfBirth < $1.yearOfBirth }

不過据途,可選值屬性和localizedStandardCompare方法結(jié)合绞愚,代碼會丑陋不堪。例如颖医,我們想用在可選值中定義的?fileExtension?屬性來對一個包含文件名的數(shù)組進行排序:

var?files = ["one",?"file.h",?"file.c",?"test.h"]?files.sort { l, r?in?r.fileExtension.flatMap {

l.fileExtension?.localizedStandardCompare($0)

?} == .orderedAscending }

files?// ["one", "file.c", "file.h", "test.h"]

改進:?讓可選值的排序稍微容易一些位衩,對多個屬性進行排序。要同時排序姓和名便脊。

方法:?用標準庫的?lexicographicallyPrecedes?方 法來進行實現(xiàn)蚂四。這個方法接受兩個序列,并對它們執(zhí)行一個電話簿方式的比較哪痰,也就是說遂赠,這 個比較將順次從兩個序列中各取一個元素來進行比較,直到發(fā)現(xiàn)不相等的元素晌杰。所以跷睦,我們可 以用姓和名構(gòu)建兩個數(shù)組,然后使用?lexicographicallyPrecedes?來比較它們肋演。我們還需要一個 函數(shù)來執(zhí)行這個比較抑诸,這里我們把使用了?localizedStandardCompare?的比較代碼放到這個函 數(shù)中:

people.sorted { p0, p1?in

let?left = [p0.last, p0.first]

let?right = [p1.last, p1.first]

return?left.lexicographicallyPrecedes(right) {

$0.localizedStandardCompare($1) == .orderedAscending?}

}

/*

[Ava Barnes (2000), Ava Barnes (1998), Robert Barnes (1985),

David Gray (1991), Joanne Miller (1994), Emily Young (2002)]?*/


不過還有很大的改進空間。?在每次比較的時候都構(gòu)建一個數(shù)組是非常沒有效率的爹殊,比較操作也是被寫死的蜕乡,通過這種方法 我們將無法實現(xiàn)對?yearOfBirth?的排序。

函數(shù)作為數(shù)據(jù)

方法:定義一個描述對象順序的函數(shù)

其中梗夸,最簡單的一種 實現(xiàn)就是接受兩個對象作為參數(shù)层玲,并在它們順序正確的時候,返回?true反症。這個函數(shù)的類型正是 標準庫中?sort(by:)?和?sorted(by:)?的參數(shù)類型辛块。接下來,讓我們先定義一個泛型別名來表達這 種函數(shù)形式的排序描述符:

///?一個排序斷言铅碍,當?shù)谝粋€值應當排在第二個值之前時润绵,返回?`true`?

typealias?SortDescriptor<Root> = (Root, Root) -> Bool

現(xiàn)在,就可以用這個別名定義比較?Person?對象的排序描述符了胞谈。它可以比較出生年份尘盼,也可以 比較姓的字符串:

let?sortByYear: SortDescriptor = { $0.yearOfBirth < $1.yearOfBirth }

?let?sortByLastName: SortDescriptor<Person> = {

$0.last.localizedStandardCompare($1.last) == .orderedAscending?

}

除了手寫這些排序描述符外,我們也可以創(chuàng)建一個函數(shù)來生成它們烦绳。將相同的屬性寫兩次并不 太好卿捎,比如在?sortByLastName?中,我們很容易就會不小心弄成?$0.last?和?$1.first?進行比較爵嗅。 而且寫排序描述符本身也挺無聊的:想要通過名來排序的時候,很可能你就把姓排序的?sortByLastName?復制粘貼一下笨蚁,然后再進行修改睹晒。

為了避免復制粘貼趟庄,我們可以定義一個函數(shù),它和?NSSortDescriptor?大體相似伪很,但不涉及運行 時編程戚啥。這個函數(shù)的第一個參數(shù)是一個名為?key?的函數(shù)此函數(shù)接受一個正在排序的數(shù)組的元 素锉试,并返回這個排序描述符所處理的屬性的值猫十。然后,我們使用第二個參數(shù)?areInIncreasingOrder?比較?key?返回的結(jié)果呆盖。最后拖云,用?SortDescriptor?把這兩個參數(shù)包裝一 下,就是要返回的排序描述符了:

/// `key`?函數(shù)应又,根據(jù)輸入的參數(shù)返回要進行比較的元素

/// `by`?進行比較的斷言

///?通過用?`by`?比較?`key`?返回值的方式構(gòu)建?`SortDescriptor`?函數(shù)?func?sortDescriptor<Root, Value>(

key: @escaping?(Root) -> Value,

by areInIncreasingOrder: @escaping?(Value, Value) -> Bool)?-> SortDescriptor<Root>

{

return?{ areInIncreasingOrder(key($0), key($1)) }

}


key?函數(shù)描述了如何深入一個?Root?類型的元素宙项,并提取出一個和特定排序步驟相關 的?Value?類型的值。因為借鑒了泛型參數(shù)名字?Root?和?Value株扛,所以它和?Swift 4?引入 的?Swift?原生鍵路徑有很多相同之處尤筐。我們會在下面討論怎么用?Swift?的鍵路徑重寫 這個方法。

有了這個函數(shù)洞就,我們就可以用另外一種方式來定義?sortByYear?了:

let?sortByYearAlt: SortDescriptor =?sortDescriptor(key: { $0.yearOfBirth }, by: <)

people.sorted(by: sortByYearAlt)

//[Robert Barnes (1985), David Gray (1991), Joanne Miller (1994),?Ava Barnes (1998), Ava Barnes (2000), Emily Young (2002)]

甚至盆繁,我們還可以為所有實現(xiàn)了?Comparable?的類型定義一個重載版本:

func?sortDescriptor<Root, Value>(key: @escaping?(Root) -> Value)?-> SortDescriptor?where?Value: Comparable

{

return?{ key($0) < key($1) }

}

let?sortByYearAlt2: SortDescriptor<Person> =

sortDescriptor(key: { $0.yearOfBirth })

這兩個?sortDescriptor?都使用了返回布爾值的排序函數(shù),因為這是標準庫中對于比較斷言的約 定旬蟋。但另一方面油昂,F(xiàn)oundation?中像是?localizedStandardCompare?這樣的?API,返回的卻是一 個包含?(升序咖为,降序秕狰,相等)?三種值的?ComparisonResult。給?sortDescriptor?增加這種支持也 很簡單:

func?sortDescriptor(

key: @escaping?(Root) -> Value,

ascending: Bool =?true,

by comparator: @escaping?(Value) -> (Value) -> ComparisonResult)?-> SortDescriptor<Root>

{

return?{ lhs, rhs?in

let?order: ComparisonResult = ascending?? .orderedAscending

: .orderedDescending

return?comparator(key(lhs))(key(rhs)) == order?}

}

這樣躁染,我們就可以用簡短清晰得多的方式來寫?sortByFirstName?了:

let?sortByFirstName: SortDescriptor =

sortDescriptor(key: { $0.first }, by: String.localizedStandardCompare)

people.sorted(by: sortByFirstName)

/*

[Ava Barnes (2000), Ava Barnes (1998), David Gray (1991),

Emily Young (2002), Joanne Miller (1994), Robert Barnes (1985)]?*/


現(xiàn)在鸣哀,SortDescriptor?和?NSSortDescriptor?就擁有了同樣地表達能力,不過它是類型安全的吞彤, 而且不依賴于運行時編程庞瘸。

OC的例子中擅憔,我們曾經(jīng)用?NSArray.sortedArray(using:)?方法指定了多個比較運算符對數(shù)組進行排序。

現(xiàn)在我們將使用一種不同的實現(xiàn)方式: 我們定義一個把多個排序描述符 合并為一個的函數(shù)。它的工作方式和?sortedArray(using:)?類似:首先它會使用第一個描述符吠勘, 并檢查比較的結(jié)果。如果相等狐援,再使用第二個贡必,第三個,直到全部用完:

func?combine

(sortDescriptors: [SortDescriptor]) -> SortDescriptor {?return?{ lhs, rhs?in

for?areInIncreasingOrder?in?sortDescriptors {

if?areInIncreasingOrder(lhs, rhs) {?return true?}?if?areInIncreasingOrder(rhs, lhs) {?return false?}

}

return false

}?}

最終把一開始的例子重寫為這樣:

et?combined: SortDescriptor<Person> = combine(

sortDescriptors: [sortByLastName, sortByFirstName, sortByYear]?)

people.sorted(by: combined)

/*

[Ava Barnes (1998), Ava Barnes (2000), Robert Barnes (1985),

David Gray (1991), Joanne Miller (1994), Emily Young (2002)]?*/

最終雹嗦,我們得到了一個與?Foundation?中的版本在行為和功能上等價的實現(xiàn)方法范舀,但是我們的 方式要更安全合是,也更符合?Swift?的語言習慣。因為?Swift?的版本不依賴于運行時編程锭环,所以編譯 器有機會對它進行更好的優(yōu)化聪全。另外,我們也可以使用它排序結(jié)構(gòu)體或非?Objective-C?的對象辅辩。

基于函數(shù)的方式有一個不足难礼,那就是函數(shù)是不透明的。我們可以獲取一個?NSSortDescriptor?并將它打印到控制臺玫锋,我們也能從排序描述符中獲得一些信息蛾茉,比如鍵路徑,selector?的名字景醇, 以及排序順序等臀稚。但是在基于函數(shù)的方式中,這些都無法做到三痰。(一些信息取不到)如果這些信息很重要的話吧寺,我 們可以將函數(shù)封裝到一個結(jié)構(gòu)體或類中,然后在其中存儲一些額外的調(diào)試信息散劫。

把函數(shù)作為數(shù)據(jù)使用的這種方式?(例如:在運行時構(gòu)建包含排序函數(shù)的數(shù)組)稚机,把語言的動態(tài)行 為帶到了一個新的高度。這使得像?Swift?這種需要編譯的靜態(tài)語言也可以實現(xiàn)諸如?Objective-C?或?Ruby?中的一部分動態(tài)特性获搏。

我們也看到了合并其他函數(shù)的函數(shù)的用武之地赖条,它也是函數(shù)式編程的構(gòu)建模塊之一。例如常熙,?combine(sortDescriptors:)?函數(shù)接受一個排序描述符的數(shù)組纬乍,并將它們合并成了單個的排序描 述符。在很多不同的應用場景下裸卫,這項技術都非常強大仿贬。

除此之外,我們甚至還可以寫一個自定義的運算符墓贿,來合并兩個排序函數(shù):

infix operator?<||> : LogicalDisjunctionPrecedence

func?<||><A>(lhs: @escaping?(A,A) -> Bool, rhs: @escaping?(A,A) -> Bool)

-> (A,A) -> Bool?{

return{x,yin

if?lhs(x, y) {?return true?}?if?lhs(y, x) {?return false?}

//?否則茧泪,它們就是一樣的,所以我們檢查第二個條件?if?rhs(x, y) {?return true?}

return false

}?}

大部分時候聋袋,自定義運算符不是什么好主意队伟。因為自定義運算符的名字無法描述行為,所以它

們通常都比函數(shù)更難理解幽勒。不過嗜侮,當使用得當?shù)臅r候,它們也會非常強大。有了上面的運算符锈颗,

我們可以重寫合并排序的例子:

let?combinedAlt = sortByLastName <||> sortByFirstName <||> sortByYear?people.sorted(by: combinedAlt)

/*

[Ava Barnes (1998), Ava Barnes (2000), Robert Barnes (1985),

David Gray (1991), Joanne Miller (1994), Emily Young (2002)]

這樣的代碼讀起來非常清晰缠借,而且可能比原來調(diào)用函數(shù)進行合并的做法更簡潔一些。不過這有?一個前提宜猜,那就是你?(和這段代碼的讀者)?都已經(jīng)習慣了該操作符的意義。相比自定義操作符的 版本硝逢,我們還是傾向于選擇?combine(sortDescriptors:)?函數(shù)姨拥。它在調(diào)用方看來更加清晰,而且 顯然增強了代碼的可讀性渠鸽。除非你正在寫一些面向特定領域的代碼叫乌,否則自定義的操作符很可 能都是在用牛刀殺雞。

再寫一 個接受函數(shù)作為參數(shù)徽缚,并返回函數(shù)的函數(shù)憨奸。這個函數(shù)可以把類似?localizedStandardCompare?這種接受兩個字符串并進行比較的普通函數(shù),提升成比較兩個字符串可選值的函數(shù)凿试。如果兩個 比較值都是nil排宰,那么它們相等。如果左側(cè)的值是?nil那婉,而右側(cè)不是的話板甘,返回升序,相反的時候 返回降序详炬。最后盐类,如果它們都不是?nil?的話,我們使用?compare?函數(shù)來對它們進行比較:

func?lift<A>(_?compare: @escaping?(A) -> (A) -> ComparisonResult) -> (A?) -> (A?)

-> ComparisonResult?{

return{lhsin{rhsin

switch?(lhs, rhs) {

case?(nil,?nil):?return?.orderedSame?case?(nil,?_):?return?.orderedAscending?case?(_,?nil):?return?.orderedDescending?case let?(l?, r?):?return?compare(l)(r)

}

}}?}

這讓我們能夠?qū)⒁粋€普通的比較函數(shù)?“提升” (lift)?到可選值的作用域中呛谜,這樣它就能夠和我們 的?sortDescriptor?函數(shù)一起使用了在跳。如果你還記得之前的?files?數(shù)組,你會知道因為需要處理 可選值的問題隐岛,按照?fileExtension?對它進行排序的代碼十分難看猫妙。不過現(xiàn)在有了新的?lift?函 數(shù),它就又變得很清晰了:

let?compare = lift(String.localizedStandardCompare)

let?result =?files.sorted(by: sortDescriptor(key: { $0.fileExtension },

by: compare))

result?// ["one", "file.c", "file.h", "test.h"]

我們可以為返回?Bool?的函數(shù)寫一個類似的?lift礼仗。在可選值一章中我們提到過吐咳,標準庫 現(xiàn)在不再為可選值提供像是?>?這樣的運算符了。因為如果你使用的不小心元践,可能會產(chǎn) 生預想之外的結(jié)果韭脊,因此它們被刪除了。Bool?版本的?lift?函數(shù)可以讓你輕而易舉地將 現(xiàn)有的運算符提升為可選值也能使用的函數(shù)单旁,以滿足你的需求沪羔。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子蔫饰,更是在濱河造成了極大的恐慌琅豆,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,718評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件篓吁,死亡現(xiàn)場離奇詭異茫因,居然都是意外死亡,警方通過查閱死者的電腦和手機杖剪,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,683評論 3 385
  • 文/潘曉璐 我一進店門冻押,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人盛嘿,你說我怎么就攤上這事洛巢。” “怎么了次兆?”我有些...
    開封第一講書人閱讀 158,207評論 0 348
  • 文/不壞的土叔 我叫張陵稿茉,是天一觀的道長。 經(jīng)常有香客問我芥炭,道長漓库,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,755評論 1 284
  • 正文 為了忘掉前任园蝠,我火速辦了婚禮米苹,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘砰琢。我一直安慰自己蘸嘶,他們只是感情好,可當我...
    茶點故事閱讀 65,862評論 6 386
  • 文/花漫 我一把揭開白布陪汽。 她就那樣靜靜地躺著训唱,像睡著了一般。 火紅的嫁衣襯著肌膚如雪挚冤。 梳的紋絲不亂的頭發(fā)上况增,一...
    開封第一講書人閱讀 50,050評論 1 291
  • 那天,我揣著相機與錄音训挡,去河邊找鬼澳骤。 笑死,一個胖子當著我的面吹牛澜薄,可吹牛的內(nèi)容都是我干的为肮。 我是一名探鬼主播,決...
    沈念sama閱讀 39,136評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼肤京,長吁一口氣:“原來是場噩夢啊……” “哼颊艳!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,882評論 0 268
  • 序言:老撾萬榮一對情侶失蹤棋枕,失蹤者是張志新(化名)和其女友劉穎白修,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體重斑,經(jīng)...
    沈念sama閱讀 44,330評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡兵睛,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,651評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了窥浪。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片卤恳。...
    茶點故事閱讀 38,789評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖寒矿,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情若债,我是刑警寧澤符相,帶...
    沈念sama閱讀 34,477評論 4 333
  • 正文 年R本政府宣布,位于F島的核電站蠢琳,受9級特大地震影響啊终,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜傲须,卻給世界環(huán)境...
    茶點故事閱讀 40,135評論 3 317
  • 文/蒙蒙 一蓝牲、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧泰讽,春花似錦例衍、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,864評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至累澡,卻和暖如春梦抢,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背愧哟。 一陣腳步聲響...
    開封第一講書人閱讀 32,099評論 1 267
  • 我被黑心中介騙來泰國打工奥吩, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人蕊梧。 一個月前我還...
    沈念sama閱讀 46,598評論 2 362
  • 正文 我出身青樓霞赫,卻偏偏與公主長得像,于是被迫代替她去往敵國和親肥矢。 傳聞我的和親對象是個殘疾皇子绩脆,可洞房花燭夜當晚...
    茶點故事閱讀 43,697評論 2 351