http://mp.weixin.qq.com/mp/getmasssendmsg?__biz=MjM5NTIyNTUyMQ==#wechat_webview_type=1&wechat_redirect
推薦序:本文是來自美團的 iOS 技術(shù)專家臧成威的投稿查描。主要介紹的是在我們的日常編程中软吐,引入高階函數(shù)所帶來的便利。順便說一下,臧老師在 StuQ 開完 RactiveCocoa 的兩次系列課程后入录,最近新開了一門 《iOS 實戰(zhàn)黑魔法》的新課程渊额,課程內(nèi)容涉及很多 Objective-C Runtime, Swift 等底層的知識和應用技巧巡蘸,如果你感興趣它褪,可以看文末的介紹。
感謝臧成威的授權(quán),以下是文章正文啤贩。
如果你開始接觸函數(shù)式編程待秃,你一定聽說過高階函數(shù)。在維基百科它的中文解釋是這樣的:
在數(shù)學和計算機科學中痹屹,高階函數(shù)是至少滿足下列一個條件的函數(shù):
- 接受一個或多個函數(shù)作為輸入
- 輸出一個函數(shù)
看起它就是 ObjC 語言中入?yún)⒒蛘叻祷刂禐?block 的 block 或者函數(shù)章郁,在 Swift 語言中即為入?yún)⒒蛘叻祷刂禐楹瘮?shù)的函數(shù)。那它們在實際的開發(fā)過程中究竟起著什么樣的作用呢志衍?我們將從入?yún)⑴⒎祷刂岛途C合使用三部分來看這個問題:
函數(shù)作為入?yún)?/h2>
函數(shù)作為入?yún)⑺坪鯚o論在 ObjC 時代還是 Swift 時代都是司空見慣的事情,例如AFNetworking
就用兩個入?yún)?block 分別回調(diào)成功與失敗楼肪。Swift 中更是加了一個尾閉包的語法(最后一個參數(shù)為函數(shù)培廓,可以不寫括號或者寫到括號外面直接跟隨方法名),例如下面這樣:
[1, 2, 3].forEach { item in
print(item)
}
我們可以將入?yún)楹瘮?shù)的函數(shù)分為兩類春叫,escaping 函數(shù)入?yún)⒑?noescape 函數(shù)入?yún)⒓缒疲瑓^(qū)別在于這個入?yún)⒌暮瘮?shù)是在執(zhí)行過程內(nèi)被調(diào)用還是在執(zhí)行過程外被調(diào)用。執(zhí)行過程外被調(diào)用的一般用于 callback 用途暂殖,例如:
Alamofire.request("https://httpbin.org/get").responseJSON {
response in
print(response.request) // original URL request
print(response.response) // HTTP URL response
print(response.data) // server data
print(response.result) // result of response serialization
if let JSON = response.result.value {
print("JSON: \(JSON)")
}
}
這個 response 的入?yún)⒑瘮?shù)就作為網(wǎng)絡(luò)請求回來的一個 callback价匠,并不會在執(zhí)行 responseJSON 這個函數(shù)的時候被調(diào)用。另外我們來觀察forEach的代碼呛每,可以推斷入?yún)⒌暮瘮?shù)一定會在forEach執(zhí)行過程中使用霞怀,執(zhí)行完就沒有利用意義,這類就是 noescape 函數(shù)莉给。
callback 的用法大家應該比較熟悉了,介紹給大家 noescape 入?yún)⒌囊恍┯梅ǎ?/p>
1. 自由構(gòu)造器
看過 GoF 設(shè)計模式的同學不知道是否還記得構(gòu)造器模式廉沮,Android 中的構(gòu)造器模式類似如下:
new AlertDialog.Builder(this)
.setIcon(R.drawable.find_daycycle_icon)
.setTitle(" 提醒 ")
.create()
.show();
如果你想要做成這樣的代碼颓遏,你需要將setIcon
、setTitle
滞时、create
等方法都實現(xiàn)成返回this
才行叁幢。這樣就無法直接利用無返回值的setter
了。
為什么需要這樣的方式呢坪稽?如果你同時有如下需求:
構(gòu)造一個對象需要很多的參數(shù)
這些參數(shù)里面很多有默認值
這些參數(shù)對應的屬性未來不希望被修改
那么用這樣的模式就可以直觀又精巧的展示構(gòu)建過程曼玩。
如果使用 noescape 入?yún)⒑瘮?shù)還可以更簡單的構(gòu)造出這種代碼,只需要傳入一個入?yún)?builder 的對象就可以了窒百,如下:
// 實現(xiàn)在這里 class SomeBuilder {
var prop1: Int
var prop2: Bool
var prop3: String
init() {
// default value
prop1 = 0
prop2 = true
prop3 = "some string"
}
}
class SomeObj {
private var prop1: Int
private var prop2: Bool
private var prop3: String
init(_ builderBlock:(SomeBuilder) -> Void) {
let someBuilder = SomeBuilder()
builderBlock(someBuilder) // noescape 入?yún)⒌氖褂?
prop1 = someBuilder.prop1
prop2 = someBuilder.prop2
prop3 = someBuilder.prop3
}
}
// 使用的時候 let someOjb = SomeObj { builder in
builder.prop1 = 15
builder.prop2 = false
builder.prop3 = "haha" }
2.自動配對操作
很多時候黍判,我們開發(fā)過程中都會遇到必須配對才能正常工作的 API,例如打開文件和關(guān)閉文件篙梢、進入 edit 模式退出 edit 模式等顷帖。雖然 swift 語言給我們 defer 這樣的語法糖避免大家忘記配對操作,但是代碼看起來還是不那么順眼
func updateTableView1() {
self.tableView.beginUpdates()
self.tableView.insertRows(at: [IndexPath(row: 2, section: 0)], with: .fade)
self.tableView.deleteRows(at: [IndexPath(row: 5, section: 0)], with: .fade)
self.tableView.endUpdates() // 容易漏掉或者上面出現(xiàn)異常 }
func updateTableView2() {
self.tableView.beginUpdates()
defer {
self.tableView.endUpdates()
}
self.tableView.insertRows(at: [IndexPath(row: 2, section: 0)], with: .fade)
self.tableView.deleteRows(at: [IndexPath(row: 5, section: 0)], with: .fade)
}
利用 noescape 入?yún)ⅲ覀兛梢詫⒁僮鞯倪^程封裝起來贬墩,使得上層看起來更規(guī)整
// 擴展一下 UITableView extension UITableView {
func updateCells(updateBlock: (UITableView) -> Void) {
beginUpdates()
defer {
endUpdates()
}
updateBlock(self)
}
}
func updateTableView() {
// 使用的時候
self.tableView.updateCells { (tableView) in
tableView.insertRows(at: [IndexPath(row: 2, section: 0)], with: .fade)
tableView.deleteRows(at: [IndexPath(row: 5, section: 0)], with: .fade)
}
}
函數(shù)作為入?yún)⒕秃唵谓榻B到這里榴嗅,下面看看函數(shù)作為返回值。
函數(shù)作為返回值
在大家的日常開發(fā)中陶舞,函數(shù)作為返回值的情況想必是少之又少嗽测。不過,如果能簡單利用起來肿孵,就會讓代碼一下子清爽很多唠粥。
首先沒有爭議的就是我們有很多的 API 都是需要函數(shù)作為入?yún)⒌模瑹o論是上一節(jié)提到過的 escaping 入?yún)⑦€是 noescape 入?yún)渚K院芏嗟臅r候厅贪,大家寫的代碼重復率會很高,例如:
let array = [1, 3, 55, 47, 92, 77, 801]
let array1 = array.filter { $0 > 3 * 3}
let array2 = array.filter { $0 > 4 * 4}
let array3 = array.filter { $0 > 2 * 2}
let array4 = array.filter { $0 > 5 * 5}
一段從數(shù)組中找到大于某個數(shù)平方的代碼雅宾,如果不封裝养涮,看起來應該是這樣的。為了簡化眉抬,通常會封裝成如下的兩個樣子:
func biggerThanPowWith(array: [Int], value: Int) -> [Int] {
return array.filter { $0 > value * value}
}
let array1 = biggerThanPowWith(array: array, value: 3)
let array2 = biggerThanPowWith(array: array, value: 4)
let array3 = biggerThanPowWith(array: array, value: 2)
let array4 = biggerThanPowWith(array: array, value: 5)
如果用高階函數(shù)的返回值函數(shù)贯吓,可以做成這樣一個高階函數(shù):
// 一個返回 (Int)->Bool 的函數(shù) func biggerThanPow2With(value: Int) -> (Int) -> Bool {
return { $0 > value * value }
}
let array1 = array.filter(biggerThanPow2With(value: 3))
let array2 = array.filter(biggerThanPow2With(value: 4))
let array3 = array.filter(biggerThanPow2With(value: 2))
let array4 = array.filter(biggerThanPow2With(value: 5))
你一定會說,兩者看起來沒啥區(qū)別蜀变。所以這里面需要講一下使用高階返回函數(shù)的幾點好處
1. 不需要 wrapper 函數(shù)也不需要打開原始類
如同上面的簡單封裝悄谐,其實就是一個 wrapper 函數(shù),把array作為入?yún)脒M來库北。這樣寫代碼和看代碼的時候就稍微不爽一點爬舰,畢竟大家都喜歡 OOP 嘛。如果要 OOP寒瓦,那就勢必要對原始類進行擴展情屹,一種方式是加 extension,或者直接給類加一個新的方法杂腰。
2. 閱讀代碼的時候一目了然
使用簡單封裝的時候垃你,看代碼的人并不知道內(nèi)部使用了filter這個函數(shù),必須要查看源碼才能知道喂很。但是用高階函數(shù)的時候惜颇,一下子就知道了使用了系統(tǒng)庫的filter。
3. 更容易復用
這也是最關(guān)鍵的一點少辣,更細粒度的高階函數(shù)凌摄,可以更方便的復用,例如我們知道 Set也是有filter這個方法的漓帅,復用起來就這樣:
let set = Set<Int>(arrayLiteral: 1, 3, 7, 9, 17, 55, 47, 92, 77, 801)
let set1 = set.filter(biggerThanPow2With(value: 3))
let set2 = set.filter(biggerThanPow2With(value: 9))
回憶下上面的簡單封裝望伦,是不是就無法重用了呢林说?
類似的返回函數(shù)的高階函數(shù)還可以有很多例子,例如上面說過的 builder屯伞,假如每次都需要定制成特殊的樣子锯梁,但是某個字段不同第岖,就可以用高階函數(shù)很容易打造出來:
func builerWithDifferentProp3(prop3: String) -> (SomeBuilder) -> Void {
return { builder in
builder.prop1 = 15
builder.prop2 = true
builder.prop3 = prop3
}
}
let someObj1 = SomeObj.init(builerWithDifferentProp3(prop3: "a"))
let someObj2 = SomeObj.init(builerWithDifferentProp3(prop3: "b"))
let someObj3 = SomeObj.init(builerWithDifferentProp3(prop3: "c"))
介紹完入?yún)⑴c返回值吗伤,還有另外的一個組合模式茁裙,那就是入?yún)⑹且粋€函數(shù),返回值也是一個函數(shù)的情況末融,我們來看看這種情況钧惧。
入?yún)⒑瘮?shù) && 返回值函數(shù)
這樣的一個函數(shù)看起來會很恐怖,swift 會聲明成func someFunc<A, B, C, D>(_ a: (A) -> B)-> (C) -> D
勾习,objective-c 會聲明成- (id (^)(id))someFunc:(id (^)(id))block
浓瞪。讓我們先從一個小的例子來講起,回憶一下我們剛剛做的biggerThanPow2With
這個函數(shù)巧婶,如果我們要一個notBiggerThanPow2With
怎么辦呢乾颁?你知道我一定不會說再寫一個。所以我告訴你我會這樣寫:
func not<T>(_ origin_func: @escaping (T) -> Bool) -> (T) -> Bool {
return { !origin_func($0) }
}
let array5 = array.filter(not(biggerThanPow2With(value: 9)))
并不需要一個notBiggerThanPow2With
函數(shù)艺栈,我們只需要實現(xiàn)一個not
就可以了英岭。它的入?yún)⑹且粋€(T) -> Boo
l,返回值也是(T) -> Bool
湿右,只需要在執(zhí)行 block 內(nèi)部的時候用個取反就可以了诅妹。這樣不單可以解決剛才的問題,還可以解決任何(T) -> Bool
類型函數(shù)的取反問題毅人,比如我們有一個odd(_: int)
方法來過濾奇數(shù)吭狡,那我們就可以用even=not(odd)
得到一個過濾偶數(shù)的函數(shù)了。
func odd(_ value: Int) -> Bool {
return value % 2 == 1 }
let array6 = array.filter(odd)
let array7 = array.filter(not(odd))
let even = not(odd)
let array8 = array.filter(even)
大家可以看下上面的biggerThanPow2With
時我們討論過的丈莺,如果biggerThanPow2With
不是一個返回函數(shù)的高階函數(shù)划煮,那它就不太容易用not
函數(shù)來加工了。
綜上场刑,如果一個入?yún)⒑头祷刂刀际呛瘮?shù)的函數(shù)就是這樣的一個轉(zhuǎn)換函數(shù),它能夠讓我們用更少的代碼組合出更多的函數(shù)蚪战。另外需要注意一下牵现,如果返回的函數(shù)里面閉包了入?yún)⒌暮瘮?shù),那么入?yún)⒑瘮?shù)就是 escaping
入?yún)⒘恕?/p>
下面再展示給大家兩個函數(shù)邀桑,一個交換參數(shù)的函數(shù)exchangeParam
瞎疼,另一個是柯里化函數(shù)currying
:
func exchangeParam<A, B, C>(_ block: @escaping (A, B) -> C) -> (B, A) -> C {
return { block($1, $0) }
}
func currying<A, B, C>(_ block: @escaping (A, B) -> C, _ value: A) -> (B) -> C {
return { block(value, $0) }
}
第一個函數(shù)exchangeParam
是交換一個函數(shù)的兩個參數(shù),第二個函數(shù)currying
是給一個帶兩個參數(shù)的函數(shù)和一個參數(shù)壁畸,返回一個帶一個參數(shù)的函數(shù)贼急。那這兩個函數(shù)究竟有什么用途呢茅茂?看一下下面的例子:
let array9 = array.filter(currying(exchangeParam(>), 9))
swift 語言里面>
是一個入?yún)?(a, b) 的函數(shù),所以>(5, 3) == true
太抓。我們使用exchangeParam
交換參數(shù)就變成了 (b, a)空闲,這時exchangeParam(>)(5, 3)
就等于false
了。
而 currying 函數(shù)又把參數(shù)b
固定為一個常量 9走敌,所以currying(exchangeParam(>), 9)
就是大于 9 的函數(shù)意思碴倾。
這個例子里就利用了全部的預制函數(shù)和通用函數(shù),沒有借助任何的命令與業(yè)務(wù)函數(shù)聲明實現(xiàn)了一個從數(shù)組中過濾大于 9 的子數(shù)組的需求掉丽。試想一下跌榔,如果我們更多的使用這樣的高階函數(shù),代碼中是不是很多的邏輯可以更容易的互相組合捶障,而這就是函數(shù)式編程的魅力僧须。
總結(jié)
高階函數(shù)的引入,無論是從函數(shù)式編程還是從非函數(shù)式編程都帶給我們代碼一定程度的簡化项炼,使得我們的 API 更加簡易可用担平,復用更充分。然而本文的例子不過是冰山一角芥挣,更多的內(nèi)容還需要大家的不斷嘗試和創(chuàng)新驱闷,也可以通過學習更多的函數(shù)式編程范式來加深理解。