就像我們?cè)谇皫坠?jié)中提到的一樣沪斟,當(dāng)你要對(duì)Array做一些處理的時(shí)候广辰,像C語(yǔ)言中類似的循環(huán)和下標(biāo)暇矫,都不是理想的選擇。Swift有一套自己的“現(xiàn)代化”手段择吊。簡(jiǎn)單來(lái)說(shuō)李根,就是用closure來(lái)參數(shù)化對(duì)數(shù)組的操作行為。這聽(tīng)著有點(diǎn)兒抽象几睛,我們從一個(gè)最簡(jiǎn)單的例子開(kāi)始房轿。
從循環(huán)到map
假設(shè)我們有一個(gè)簡(jiǎn)單的Fibonacci序列:[0, 1, 1, 2, 3, 5]。如果我們要計(jì)算每個(gè)元素的平方所森,怎么辦呢囱持?
一個(gè)最樸素的做法是for循環(huán):
var fibonacci = [0, 1, 1, 2, 3, 5]
var squares = [Int]()
for value in fibonacci {
squares.append(value * value)
}
也許,現(xiàn)在你還覺(jué)得這樣沒(méi)什么不好理解焕济,但是纷妆,想象一下這段代碼在幾十行代碼中間的時(shí)候,或者當(dāng)這樣類似的邏輯反復(fù)出現(xiàn)的時(shí)候晴弃,整體代碼的可讀性就不那么強(qiáng)了掩幢。
如果你覺(jué)得這還不是個(gè)足夠引起你注意的問(wèn)題,那么上鞠,當(dāng)我們要定義一個(gè)常量squares的時(shí)候际邻,上面的代碼就完全無(wú)法勝任了。怎么辦呢旗国?先來(lái)看解決方案:
// [0, 1, 1, 4, 9, 25]
let constSquares = fibonacci.map { $0 * $0 }
上面這行代碼枯怖,和之前那段for循環(huán)執(zhí)行的結(jié)果是相同的。顯然能曾,它比f(wàn)or循環(huán)更具表現(xiàn)力度硝,并且也能把我們期望的結(jié)果定義成常量。當(dāng)然寿冕,map并不是什么魔法蕊程,無(wú)非就是把for循環(huán)執(zhí)行的邏輯,封裝在了函數(shù)里驼唱,這樣我們就可以把函數(shù)的返回值賦值給常量了藻茂。我們可以通過(guò)extension很簡(jiǎn)單的自己來(lái)實(shí)現(xiàn)map:
extension Array {
func myMap<T>(_ transform: (Element) -> T) -> [T] {
var tmp: [T] = []
tmp.reserveCapacity(count)
for value in self {
tmp.append(transform(value))
}
return tmp
}
}
雖然和Swift標(biāo)準(zhǔn)庫(kù)相比,myMap的實(shí)現(xiàn)中去掉了和異常聲明相關(guān)的部分玫恳。但它已經(jīng)足以表現(xiàn)map的核心實(shí)現(xiàn)過(guò)程了辨赐。除了在append之前使用了reserveCapacity給新數(shù)組預(yù)留了空間之外,它的實(shí)現(xiàn)過(guò)程和一開(kāi)始我們使用的for循環(huán)沒(méi)有任何差別京办。
如果你還不了解Element也沒(méi)關(guān)系掀序,把它理解為Array中元素類型的替代符就好了。在后面我們講到Sequence類型的時(shí)候惭婿,會(huì)專門(mén)提到它不恭。
完成后叶雹,當(dāng)我們?cè)趐layground里測(cè)試的時(shí)候:
// [0, 1, 1, 4, 9, 25]
let constSequence1 = fibonacci.myMap { $0 * $0 }
就會(huì)發(fā)現(xiàn)執(zhí)行結(jié)果和之前的constSequence是一樣的了。
參數(shù)化數(shù)組元素的執(zhí)行動(dòng)作
其實(shí)换吧,仔細(xì)觀察myMap的實(shí)現(xiàn)折晦,就會(huì)發(fā)現(xiàn)它最大的意義,就是保留了遍歷Array的過(guò)程沾瓦,而把要執(zhí)行的動(dòng)作留給了myMap的調(diào)用者通過(guò)參數(shù)去定制满着。而這,就是我們一開(kāi)始提到的用closure來(lái)參數(shù)化對(duì)數(shù)組的操作行為的含義暴拄。
有了這種思路之后漓滔,我們就可以把各種常用的帶有遍歷行為的操作,定制成多種不同的遍歷“套路”乖篷,而把對(duì)數(shù)組中每一個(gè)元素的處理動(dòng)作留給函數(shù)的調(diào)用者响驴。但是別急,在開(kāi)始自動(dòng)動(dòng)手造輪子之前撕蔼,Swift library已經(jīng)為我們準(zhǔn)備了一些豁鲤,例如:
首先,是找到最小鲸沮、最大值琳骡,對(duì)于這類操作來(lái)說(shuō),只要數(shù)組中的元素實(shí)現(xiàn)了Equatable protocol讼溺,我們甚至無(wú)需定義對(duì)元素的具體操作:
fibonacci.min() // 0
fibonacci.max() // 5
使用min和max很安全楣号,因?yàn)楫?dāng)數(shù)組為空時(shí),這兩個(gè)方法將返回nil怒坯。
其次炫狱,過(guò)濾出滿足特定條件的元素,我們只要通過(guò)參數(shù)指定篩選規(guī)則就好了:
fibonacci.filter { $0 % 2 == 0 }
第三剔猿,比較數(shù)組相等或以特定元素開(kāi)始视译。對(duì)這類操作,我們需要提供兩個(gè)內(nèi)容归敬,一個(gè)是要比較的數(shù)組酷含,另一個(gè)則是比較的規(guī)則:
// false
fibonacci.elementsEqual([0, 1, 1], by: { $0 == $1 })
// true
fibonacci.starts(with: [0, 1, 1], by: { $0 == $1 })
第四,最原始的for循環(huán)的替代品:
fibonacci.forEach { print($0) }
// 0
// 1
// ...
要注意它和map的一個(gè)重要區(qū)別:forEach并不處理closure參數(shù)的返回值汪茧。因此它只適合用來(lái)對(duì)數(shù)組中的元素進(jìn)行一些操作椅亚,而不能用來(lái)產(chǎn)生返回結(jié)果。
第五舱污、對(duì)數(shù)組進(jìn)行排序什往,這時(shí),我們需要通過(guò)參數(shù)指定的是排序規(guī)則:
// [0, 1, 1, 2, 3, 5]
fibonacci.sorted()
// [5, 3, 2, 1, 1, 0]
fibonacci.sorted(by: >)
let pivot = fibonacci.partition(by: { $0 < 1 })
fibonacci[0 ..< pivot] // [5, 1慌闭,1别威,2, 3]
fibonacci[pivot ..< fibonacci.endIndex] // [0]
其中,sorted(by:)的用法是很直接的驴剔,它默認(rèn)采用升序排列省古。同時(shí),也允許我們通過(guò)by自定義排序規(guī)則丧失。在這里>是{ 1 }的簡(jiǎn)寫(xiě)形式豺妓。Swift中有很多在不影響語(yǔ)義的情況下的簡(jiǎn)寫(xiě)形式。
而partition(by:)則會(huì)先對(duì)傳遞給它的數(shù)組進(jìn)行重排布讹,然后根據(jù)指定的條件在重排的結(jié)果中返回一個(gè)分界點(diǎn)位置琳拭。這個(gè)分界點(diǎn)分開(kāi)的兩部分中,前半部分的元素都不滿足指定條件描验;后半部分都滿足指定條件白嘁。而后,我們就可以使用range operator來(lái)訪問(wèn)這兩個(gè)區(qū)間形成的Array對(duì)象膘流。大家可以根據(jù)例子中注釋的結(jié)果絮缅,來(lái)理解partition的用法。
第六呼股,是把數(shù)組的所有內(nèi)容耕魄,“合并”成某種形式的值,對(duì)這類操作彭谁,我們需要指定的吸奴,是合并前的初始值,以及“合并”的規(guī)則缠局。例如则奥,我們計(jì)算fibonacci中所有元素的和:
fibonacci.reduce(0, +) // 12
在這里,初始值是0甩鳄,和第二個(gè)參數(shù)+逞度,則是{ 1 }的縮寫(xiě)。
通過(guò)這些例子妙啃,你應(yīng)該能感受到了档泽,這些通過(guò)各種形式封裝了遍歷動(dòng)作的方法,它們之中的任何一個(gè)揖赴,都比直接通過(guò)for循環(huán)實(shí)現(xiàn)具有更強(qiáng)的表現(xiàn)力馆匿。這些API,開(kāi)始讓我們的代碼從面向機(jī)器的燥滑,轉(zhuǎn)變成面向業(yè)務(wù)需求的渐北。因此,在Swift里铭拧,你應(yīng)該試著讓自己轉(zhuǎn)變觀念赃蛛,當(dāng)你面對(duì)一個(gè)Array時(shí)恃锉,你真的幾乎可以忘記下標(biāo)和循環(huán)了。
區(qū)分修改外部變量和保存內(nèi)部狀態(tài)
當(dāng)我們使用上面提到的這些帶有closure參數(shù)的Array方法時(shí)呕臂,一個(gè)不好的做法就是通過(guò)closure去修改外部變量破托,并依賴這種副作用產(chǎn)生的結(jié)果。來(lái)看一個(gè)例子:
var sum = 0
let constSquares2 = fibonacci.map { (fib: Int) -> Int in
sum += fib
return fib * fib
}
在這個(gè)例子里歧蒋,map的執(zhí)行產(chǎn)生了一個(gè)副作用土砂,就是對(duì)fibonacci中所有的元素求和。這不是一個(gè)好的方法谜洽,我們應(yīng)該避免這樣萝映。你應(yīng)該單獨(dú)使用reduce來(lái)完成這個(gè)操作,或者如果一定要在closure參數(shù)里修改外部變量阐虚,哪怕用forEach也是比map更好的方案序臂。
但是,在函數(shù)實(shí)現(xiàn)內(nèi)部敌呈,專門(mén)用一個(gè)外部變量來(lái)保存closure參數(shù)的執(zhí)行狀態(tài)贸宏,則是一個(gè)常用的實(shí)現(xiàn)技法。例如磕洪,我們要?jiǎng)?chuàng)建一個(gè)新的數(shù)組吭练,其中每個(gè)值,都是數(shù)組當(dāng)前位置和之前所有元素的和析显,可以這樣:
extension Array {
func accumulate<T>(_ initial: T,
_ nextSum: (T, Element) -> T) -> [T] {
var sum = initial
return map { next in
sum = nextSum(sum, next)
return sum
}
}
}
在上面這個(gè)例子里鲫咽,我們利用map的closure參數(shù)捕獲了sum,這樣就保存了每一次執(zhí)行map時(shí)谷异,之前所有元素的和分尸。
// [0, 1, 2, 4, 7, 12]
fibonacci.accumulate(0, +)
What's next?
在這一節(jié)中,我們向大家介紹了Swift中歹嘹,使用Array最重要的一個(gè)思想:通過(guò)closure來(lái)參數(shù)化對(duì)數(shù)組的操作行為箩绍。在Swift標(biāo)準(zhǔn)庫(kù)中,基于這個(gè)思想尺上,為我們提供了在各種常用數(shù)組操作場(chǎng)景中的API材蛛。因此,當(dāng)你下意識(shí)的開(kāi)始用一個(gè)循環(huán)處理數(shù)組時(shí)怎抛,讓自己停一下卑吭,去看看Array的官方文檔,你一定可以找到更現(xiàn)代化的處理方法马绝。在下一節(jié)豆赏,我們將著重了解一下標(biāo)準(zhǔn)庫(kù)中的三個(gè)API:filter、reduce和flatMap。之所以選擇它們掷邦,是因?yàn)閒ilter和map是構(gòu)成其它各種API的基礎(chǔ)白胀,而flatMap則不太容易理解。