學習目的:?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ù)单旁,以滿足你的需求沪羔。