當你要對Array
做一些處理的時候,像C語言中類似的循環(huán)和下標,都不是理想的選擇。Swift有一套自己的“現(xiàn)代化”手段摇展。簡單來說,就是用closure來參數(shù)化對數(shù)組的操作行為溺忧。這聽著有點兒抽象咏连,從一個最簡單的例子開始來試著理解一下。
從循環(huán)到map
假設(shè)我們有一個簡單的Fibonacci序列:[0, 1, 1, 2, 3, 5]
鲁森。如果我們要計算每個元素的平方祟滴,怎么辦呢?
一個最樸素的做法是for
循環(huán):
var fibonacci = [0, 1, 1, 2, 3, 5]
var squares = [Int]()
for value in fibonacci {
squares.append(value * value)
}
也許刀森,現(xiàn)在你還覺得這樣沒什么不好理解踱启,但是,想象一下這段代碼在幾十行代碼中間的時候研底,或者當這樣類似的邏輯反復(fù)出現(xiàn)的時候埠偿,整體代碼的可讀性就不那么強了。
如果你覺得這還不是個足夠引起你注意的問題榜晦,那么冠蒋,當我們要定義一個常量squares
的時候,上面的代碼就完全無法勝任了乾胶。怎么辦呢抖剿?先來看解決方案:
// [0, 1, 1, 4, 9, 25]
let constSquares = fibonacci.map { $0 * $0 }
上面這行代碼,和之前那段for
循環(huán)執(zhí)行的結(jié)果是相同的识窿。顯然斩郎,它比for
循環(huán)更具表現(xiàn)力,并且也能把我們期望的結(jié)果定義成常量喻频。當然缩宜,map
并不是什么魔法,無非就是把for
循環(huán)執(zhí)行的邏輯,封裝在了函數(shù)里锻煌,這樣我們就可以把函數(shù)的返回值賦值給常量了妓布。我們可以通過extension
很簡單的自己來實現(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標準庫相比,myMap
的實現(xiàn)中去掉了和異常聲明相關(guān)的部分宋梧。但它已經(jīng)足以表現(xiàn)map
的核心實現(xiàn)過程了匣沼。除了在append
之前使用了reserveCapacity
給新數(shù)組預(yù)留了空間之外,它的實現(xiàn)過程和一開始我們使用的for
循環(huán)沒有任何差別捂龄。
如果你還不了解
Element
也沒關(guān)系释涛,把它理解為Array
中元素類型的替代符就好了。在后面我們講到Sequence
類型的時候倦沧,會專門提到它枢贿。
完成后,當我們在playground里測試的時候:
// [0, 1, 1, 4, 9, 25]
let constSequence1 = fibonacci.myMap { $0 * $0 }
就會發(fā)現(xiàn)執(zhí)行結(jié)果和之前的constSequence
是一樣的了刀脏。
參數(shù)化數(shù)組元素的執(zhí)行動作
其實,仔細觀察myMap
的實現(xiàn)超凳,就會發(fā)現(xiàn)它最大的意義愈污,就是保留了遍歷Array
的過程,而把要執(zhí)行的動作留給了myMap
的調(diào)用者通過參數(shù)去定制轮傍。而這暂雹,就是我們一開始提到的用closure來參數(shù)化對數(shù)組的操作行為的含義。
有了這種思路之后创夜,我們就可以把各種常用的帶有遍歷行為的操作杭跪,定制成多種不同的遍歷“套路”,而把對數(shù)組中每一個元素的處理動作留給函數(shù)的調(diào)用者驰吓。但是別急涧尿,在開始自動動手造輪子之前,Swift library已經(jīng)為我們準備了一些檬贰,例如:
首先姑廉,是找到最小、最大值翁涤,對于這類操作來說桥言,只要數(shù)組中的元素實現(xiàn)了Equatable
protocol,我們甚至無需定義對元素的具體操作:
fibonacci.min() // 0
fibonacci.max() // 5
使用min
和max
很安全葵礼,因為當數(shù)組為空時号阿,這兩個方法將返回nil
。
其次鸳粉,過濾出滿足特定條件的元素扔涧,我們只要通過參數(shù)指定篩選規(guī)則就好了:
fibonacci.filter { $0 % 2 == 0 }
第三,比較數(shù)組相等或以特定元素開始赁严。對這類操作扰柠,我們需要提供兩個內(nèi)容粉铐,一個是要比較的數(shù)組,另一個則是比較的規(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
的一個重要區(qū)別:forEach
并不處理closure參數(shù)的返回值蝙泼。因此它只適合用來對數(shù)組中的元素進行一些操作,而不能用來產(chǎn)生返回結(jié)果劝枣。
第五汤踏、對數(shù)組進行排序,這時舔腾,我們需要通過參數(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:)
的用法是很直接的哗脖,它默認采用升序排列。同時扳还,也允許我們通過by
自定義排序規(guī)則才避。在這里>
是{ $0 > $1 }
的簡寫形式。Swift中有很多在不影響語義的情況下的簡寫形式氨距。
而partition(by:)
則會先對傳遞給它的數(shù)組進行重排桑逝,然后根據(jù)指定的條件在重排的結(jié)果中返回一個分界點位置。這個分界點分開的兩部分中俏让,前半部分的元素都不滿足指定條件楞遏;后半部分都滿足指定條件。而后首昔,我們就可以使用range operator來訪問這兩個區(qū)間形成的Array
對象寡喝。大家可以根據(jù)例子中注釋的結(jié)果,來理解partition的用法沙廉。
第六拘荡,是把數(shù)組的所有內(nèi)容,“合并”成某種形式的值撬陵,對這類操作珊皿,我們需要指定的,是合并前的初始值巨税,以及“合并”的規(guī)則蟋定。例如,我們計算fibonacci
中所有元素的和:
fibonacci.reduce(0, +) // 12
在這里草添,初始值是0驶兜,和第二個參數(shù)+
,則是{ $0 + $1 }
的縮寫。
通過這些例子抄淑,你應(yīng)該能感受到了屠凶,這些通過各種形式封裝了遍歷動作的方法,它們之中的任何一個肆资,都比直接通過for
循環(huán)實現(xiàn)具有更強的表現(xiàn)力矗愧。這些API,開始讓我們的代碼從面向機器的郑原,轉(zhuǎn)變成面向業(yè)務(wù)需求的唉韭。因此,在Swift里犯犁,你應(yīng)該試著讓自己轉(zhuǎn)變觀念属愤,當你面對一個Array
時,你真的幾乎可以忘記下標和循環(huán)了酸役。
區(qū)分修改外部變量和保存內(nèi)部狀態(tài)
當我們使用上面提到的這些帶有closure參數(shù)的Array
方法時住诸,一個不好的做法就是通過closure去修改外部變量,并依賴這種副作用產(chǎn)生的結(jié)果涣澡。來看一個例子:
var sum = 0
let constSquares2 = fibonacci.map { (fib: Int) -> Int in
sum += fib
return fib * fib
}
在這個例子里只壳,map
的執(zhí)行產(chǎn)生了一個副作用,就是對fibonacci
中所有的元素求和暑塑。這不是一個好的方法,我們應(yīng)該避免這樣锅必。你應(yīng)該單獨使用reduce
來完成這個操作事格,或者如果一定要在closure參數(shù)里修改外部變量,哪怕用forEach
也是比map
更好的方案搞隐。
但是驹愚,在函數(shù)實現(xiàn)內(nèi)部,專門用一個外部變量來保存closure參數(shù)的執(zhí)行狀態(tài)劣纲,則是一個常用的實現(xiàn)技法逢捺。例如,我們要創(chuàng)建一個新的數(shù)組癞季,其中每個值劫瞳,都是數(shù)組當前位置和之前所有元素的和,可以這樣:
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
}
}
}
在上面這個例子里绷柒,我們利用map
的closure參數(shù)捕獲了sum
志于,這樣就保存了每一次執(zhí)行map
時,之前所有元素的和废睦。
// [0, 1, 2, 4, 7, 12]
fibonacci.accumulate(0, +)
總結(jié)
Swift中伺绽,使用Array
最重要的一個思想:通過closure來參數(shù)化對數(shù)組的操作行為。在Swift標準庫中,基于這個思想奈应,為我們提供了在各種常用數(shù)組操作場景中的API澜掩。因此,當你下意識的開始用一個循環(huán)處理數(shù)組時杖挣,讓自己停一下肩榕,去看看Array
的官方文檔,你一定可以找到更現(xiàn)代化的處理方法程梦。