2.4 Swift 3 通過(guò)closure參數(shù)化對(duì)數(shù)組元素的變形操作

就像我們?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ī)則丧失。在這里>是{ 0 >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ù)+逞度,則是{ 0 +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則不太容易理解。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末耙饰,一起剝皮案震驚了整個(gè)濱河市纹笼,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌苟跪,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,110評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件蔓涧,死亡現(xiàn)場(chǎng)離奇詭異件已,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)元暴,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,443評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén)篷扩,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人茉盏,你說(shuō)我怎么就攤上這事鉴未。” “怎么了鸠姨?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,474評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵铜秆,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我讶迁,道長(zhǎng)连茧,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,881評(píng)論 1 295
  • 正文 為了忘掉前任巍糯,我火速辦了婚禮啸驯,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘祟峦。我一直安慰自己罚斗,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,902評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布宅楞。 她就那樣靜靜地躺著针姿,像睡著了一般。 火紅的嫁衣襯著肌膚如雪咱筛。 梳的紋絲不亂的頭發(fā)上搓幌,一...
    開(kāi)封第一講書(shū)人閱讀 51,698評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音迅箩,去河邊找鬼溉愁。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的拐揭。 我是一名探鬼主播撤蟆,決...
    沈念sama閱讀 40,418評(píng)論 3 419
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼堂污!你這毒婦竟也來(lái)了家肯?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 39,332評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤盟猖,失蹤者是張志新(化名)和其女友劉穎讨衣,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體式镐,經(jīng)...
    沈念sama閱讀 45,796評(píng)論 1 316
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡反镇,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,968評(píng)論 3 337
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了娘汞。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片歹茶。...
    茶點(diǎn)故事閱讀 40,110評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖你弦,靈堂內(nèi)的尸體忽然破棺而出惊豺,到底是詐尸還是另有隱情,我是刑警寧澤禽作,帶...
    沈念sama閱讀 35,792評(píng)論 5 346
  • 正文 年R本政府宣布尸昧,位于F島的核電站,受9級(jí)特大地震影響领迈,放射性物質(zhì)發(fā)生泄漏彻磁。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,455評(píng)論 3 331
  • 文/蒙蒙 一狸捅、第九天 我趴在偏房一處隱蔽的房頂上張望衷蜓。 院中可真熱鬧,春花似錦尘喝、人聲如沸磁浇。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,003評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)置吓。三九已至,卻和暖如春缔赠,著一層夾襖步出監(jiān)牢的瞬間衍锚,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,130評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工嗤堰, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留戴质,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,348評(píng)論 3 373
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像告匠,于是被迫代替她去往敵國(guó)和親戈抄。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,047評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容