數(shù)組和可變性
在Swift中最常見的集合類型非數(shù)組莫屬隆判。數(shù)組是一系列相同類型的元素的有序的容器沐飘,對于其中每個元素立磁,我們可以使用下標(biāo)對其直接進行訪問(這又被稱作隨機訪問)尤蒿。舉個例子夺克,要創(chuàng)建一個數(shù)字的數(shù)組嘀粱,我們可以這么寫:
// 斐波那契數(shù)列
let fibs = [0,1,1,2,3,5]
要是我們使用像是append(_:)這樣的方法來修改上面定義的數(shù)組的話提针,會得到一個編譯錯誤攒暇。這是因為在上面的代碼中數(shù)組使用let
聲明為常量的海雪。在很多情景下锦爵,這是正確的做法,它可以避免我們不小心對數(shù)組做出改變奥裸。如果我們想按照變量的方式來使用數(shù)組险掀,我們需要將它用var
來進行定義:
var mutableFibs = [0,1,1,2,3,5]
現(xiàn)在我們就能很容易地為數(shù)字添加單個或是一系列元素了:
mutableFibs.append(8)
mutableFibs.append(contentsOf: [13, 21])
mutableFibs // [0, 1, 1, 2, 3, 5, 8, 13, 21]
區(qū)別使用var
和let
可以給我們帶來不少好處。使用let
定義的變量因為其具有不變性湾宙,因此更有理由被優(yōu)先使用樟氢。當(dāng)你讀到類似let fibs = ...
這樣的聲明時冈绊,你可以確定fibs的值將永遠不變,這一點是由編譯器強制保證的埠啃。這在你需要通讀代碼的時候會很有幫助死宣。不過,要注意這只針對那些具有值語義的類型碴开。使用let
定義的類實例對象(也就是說對于引用類型)時毅该,它保證的是這個引用永遠不會發(fā)生變化,你不能在給這個引用賦一個新的值潦牛,但是這個引用所指向的對象卻是可以改變的眶掌。
數(shù)組和標(biāo)準(zhǔn)庫中的所有集合類型一樣,是具有值語義的巴碗。當(dāng)你創(chuàng)建一個新的數(shù)組變量并且把一個已經(jīng)存在的數(shù)組復(fù)制給他的時候朴爬,這個數(shù)組的內(nèi)容會被復(fù)制。舉個例子橡淆,在下面的代碼中寝殴,x將不會被更改:
var x = [1,2,3]
var y = x
y.append(4)
y //[1,2,3,4]
x //[1,2,3]
var y = x
語句復(fù)制了x,所以在將4添加到y(tǒng)末尾的時候明垢,x并不會發(fā)生改變蚣常,它的值依然是[1,2,3]。當(dāng)你把一個數(shù)組傳遞給一個函數(shù)時痊银,會發(fā)生同樣的事情抵蚊;方法將得到這個數(shù)組的一份本地復(fù)制,所有對它的改變都不會影響調(diào)用者所持有的數(shù)組溯革。
對比一下Foundation框架中的NSArray在可變性上的處理方法贞绳。NSArray中沒有更改方法,想要更改一下數(shù)組致稀,你必須使用NSMutableArray冈闭。但是,就算你擁有的是一個不可變的NSArray抖单,但是它的引用特性并不能保證這個數(shù)組不會被改變:
let a = NSMutableArray(array: [1,2,3])
//我們不想讓b發(fā)生改變
let b: NSArray = a
//但是事實上他依然能夠被a影響并改變
a.insert(4,at: 3)
b//(1,2,3,4)
//正確的方式在賦值時萎攒,先手動進行復(fù)制:
let c = NSMutableArray(array: [1,2,3])
//我們不想讓d發(fā)生改變
let d = c.copy() as! NSArray
c.insert(4,at: 3)
d//(1,2,3)
在上面的例子中,顯而易見矛绘,我們需要進行復(fù)制耍休,因為a的聲明畢竟是可變的。但是货矮,當(dāng)把數(shù)組在方法和函數(shù)之間來回傳遞的時候羊精,事情可能就不那么明顯了。
而在Swift中囚玫,相較于 NSArray 和 NSMutableArray倆種類型喧锦,數(shù)組只有一種統(tǒng)一的類型读规,那就是Array。使用var
可以將數(shù)組定義為可變燃少,但是區(qū)別于與NS的數(shù)組掖桦,當(dāng)你使用let
定義第二個數(shù)組,并將第一個數(shù)組賦值給它供汛,也可以保證這個新的數(shù)組是不會改變的枪汪,因為這里沒有共用的引用。
創(chuàng)建如此多的復(fù)制有可能造成性能問題怔昨,不過實際上Swift標(biāo)準(zhǔn)庫中的所有集合類型都使用了“寫時復(fù)制”這一技術(shù)雀久,它能夠保證只在必要的時候?qū)?shù)據(jù)進行復(fù)制。在我們的例子中趁舀,直到y(tǒng).append被調(diào)用的之前赖捌,x和y都將共享內(nèi)部的存儲。在結(jié)構(gòu)體和類中我們也將仔細的研究值語義矮烹,并告訴你如何為你自己的類型實現(xiàn)寫時復(fù)制特性越庇。
數(shù)組和可選值
Swift數(shù)組提供了你能想到的所有常規(guī)操作方法,像是isEmpty或是count奉狈。數(shù)組也允許直接使用特定的瞎編直接訪問其中的元素卤唉,像是fibs[3]。不過要牢記在使用下標(biāo)回去元素之前仁期,你需要確保索引值沒有超出范圍桑驱。比如取索引值為3的元素,你需要保證數(shù)組中至少有4個元素跛蛋。否則熬的,你的程序會崩潰。
這么設(shè)計的主要原因是我們可以數(shù)組切片赊级。在Swift中押框,計算一個索引值這種操作是非常罕見的:
→ 想要迭代數(shù)組 ?for x in array
→ 想要迭代除了第一個元素以外的數(shù)組其余部分理逊? for x in array.dropFirst()
→ 想要迭代除了最后5個元素以外的數(shù)組橡伞? for x in array.dropLast(5)
→ 想要列舉數(shù)組中的元素和對應(yīng)的下標(biāo)?for ( num, element) in collection.enumerated()
→想要尋找一個指定元素的位置 if let idx = array.index { someMatchingLogic($0) }
→想要對數(shù)組中的所有元素進行變形挡鞍?array.map{someTransformation($0)}
→想要篩選出符合某個標(biāo)準(zhǔn)的元素骑歹? array. filter { someCriteria($0) }
在Swift 3中傳統(tǒng)的C風(fēng)格的for
循環(huán)被移除了,這是Swift不鼓勵你去做索引計算的另一個標(biāo)志墨微。手動計算和使用索引值往往可能帶來很多潛在的bug,所以最好避免這么做扁掸。如果這不可以避免的話翘县,我們可以很容易寫一個可重用的通用函數(shù)來進行處理最域,在其中你可以對精心測試后的索引計算進行封裝,我們將在泛型一章里看到這個例子锈麸。
但是有些時候你然后不得不使用索引镀脂。對于數(shù)組索引來說,當(dāng)你這么做時忘伞,你應(yīng)該已經(jīng)深思熟慮薄翅,對背后的索引計算邏輯進行過認真思考。在這個前提下氓奈,如果每次都要對獲取的結(jié)果進行解包的話就顯得多余了——因為這意味著你不信任你的代碼翘魄。但實際上你是信任你自己的代碼的,所以你可能會選擇將結(jié)果進行強制解包舀奶,因為你知道這些下標(biāo)都是有效的暑竟,這一方面十分麻煩,另一方面也是一個壞習(xí)慣育勺。當(dāng)強制借唄編程一種習(xí)慣后但荤,很可能你會不小心強制解包了本來不應(yīng)該解包的東西,所以涧至,為了避免這個行為變成習(xí)慣腹躁,數(shù)組根本沒有給你可選值的選項。
無效的下標(biāo)操作會造成可控的崩潰南蓬,有時候這種行為可能會被叫做不安全潜慎,但是這只是安全性的一方面。下標(biāo)操作在內(nèi)存安全的意義上是完全安全的蓖康,標(biāo)準(zhǔn)庫中的集合總是會執(zhí)行邊界檢查铐炫,并禁止那些越界索引對內(nèi)存的訪問。
其他操作的行為略有不同蒜焊。first和last屬性返回的是可選值倒信,當(dāng)數(shù)組為空時,他們返回nil泳梆。first相當(dāng)于isEmpty鳖悠?nil : self[0]
。類似地优妙,如果數(shù)組為空時乘综,removeLast將會導(dǎo)致崩潰,而popLast將在數(shù)組不為空是刪除最后一個元素返回它套硼,在數(shù)組為空時卡辰,它將不執(zhí)行任何操作,直接返回nil。你應(yīng)該根據(jù)自己的需要來選取到底使用那個一個:當(dāng)你將數(shù)組當(dāng)作棧來使用時九妈,你可能總是想要將empty檢查和移除最后元素組合起來使用反砌;而另一方面,如果你已經(jīng)知道數(shù)組一定非空萌朱,那再去處理可選值就完全沒有必要宴树。
數(shù)組變形
map
對數(shù)組中的每個執(zhí)行轉(zhuǎn)換操作是一個很常見的任務(wù)。每個程序員可能都寫過上百次這樣的代碼:創(chuàng)建一個新的數(shù)組晶疼,對已有數(shù)組中的元素進行循環(huán)依次取出其中的元素酒贬,對取出的元素進行操作,并把操作的結(jié)果加入到新數(shù)組的末尾翠霍。比如锭吨,下面的代碼計算了一個整數(shù)數(shù)組里的元素的平方:
let fibs = [0,1,1,2,3,5]
var squared: [Int] = []
for fib in fibs {
squared.append(fib * fib)
}
squared//[0,1,1,4,9,25]
Swift數(shù)組擁有的map方法,這個方法來自函數(shù)式編程的世界壶运。下面的例子使用了map來完成同樣的操作:
let squares = fibs.map { fib in fib * fib }
squares//[0,1,1,4,9,25]
這種版本有三大優(yōu)勢耐齐。首先,他很短蒋情。長度短一般意味著錯誤很少埠况,不過更重要的是,它比原來更清晰棵癣。所有無關(guān)的內(nèi)容都被移除了辕翰,一旦你習(xí)慣了map滿天飛的世界,你就會發(fā)現(xiàn)map就像是一個信號狈谊,一旦你看到它喜命,就會知道即將有一個函數(shù)被作用在數(shù)組的每個元素上,并返回另一個數(shù)組河劝,它將包含所有被轉(zhuǎn)換后的結(jié)果壁榕。
其次,squared將由map的結(jié)果得到赎瞎,我們不會再改變它的值牌里,所有也就不再需要用var
來進行聲明了,我們可以將其聲明為let
务甥。另外牡辽,由于數(shù)組元素的類型可以從傳遞給map的函數(shù)中推斷出來,我們也不在需要為squared顯示的指明類型了敞临。
最后态辛,創(chuàng)造map函數(shù)并不難,你只需要把for循環(huán)中的代碼模塊部分用一個泛型函數(shù)分裝起來就可以了挺尿。下面是一種可能的實現(xiàn)方式(在Swift中奏黑,它實際上是Sequence的一個擴展炊邦,我們將在之后關(guān)于編寫泛型算法的章節(jié)里面繼續(xù)Sequence的話題):
extension Array {
func map<T>(_ transform:(Element)->T) -> [T] {
var result: [T] = []
result.reserveCapacity(count)
for x in self {
result.append(transform(x))
}
return result
}
}
Element是數(shù)組中包含的元素類型的占位符,T是元素轉(zhuǎn)換之后的類型的占位符攀涵。map函數(shù)本身并不關(guān)系Element和T究竟是什么铣耘,它們可以是任意類型的洽沟。T的具體類型將由調(diào)用者傳入給map的transform方法的返回值類型來決定以故。
實際上,這個函數(shù)的簽名應(yīng)該是
func map<T>(_ transform:(Element)->T) -> [T]
也就是說裆操,對于可能拋出錯誤的變形函數(shù)怒详,map將會把錯誤轉(zhuǎn)發(fā)給調(diào)用者。我們會在出錯誤處理一章里覆蓋這個細節(jié)踪区。在這里昆烁,我們選擇去掉錯誤處理的這個修飾,這樣看起來會更簡單一些缎岗。如果你感興趣静尼,可以看看GitHub上Swift倉庫的Sequence.map的源碼實現(xiàn)
使用函數(shù)將行為參數(shù)化
即使你已經(jīng)很熟悉map了,也請花一點時間來想一想map的代碼传泊。是什么讓它可以如此通用而且有用鼠渺?
map可以將模板代碼分離出來,這些模板代碼并不會隨著每次調(diào)用發(fā)生變動眷细,發(fā)生變動的是那些功能代碼拦盹,也就是如何變換每個元素的邏輯代碼。map函數(shù)通過接受調(diào)用者所提供的變換函數(shù)作為參數(shù)來做到這一點溪椎。
縱觀標(biāo)準(zhǔn)庫普舆,我們可以發(fā)現(xiàn)很多這樣將行為進行參數(shù)化的設(shè)計模式。標(biāo)準(zhǔn)庫中有不下十多個函數(shù)接受調(diào)用者傳入的閉包校读,并將它作為函數(shù)執(zhí)行關(guān)鍵步驟:
→ map和flatMap —— 如何對元素進行變換
→filter——元素是否應(yīng)該被包含在結(jié)果中
→ reduce——如何將元素合并到一個總和的值中
→ sequence——序列中下一個元素應(yīng)該是什么沼侣?
→ forEach——對于一個元素,應(yīng)該執(zhí)行怎么樣的操作
→ sort歉秫,lexicographicCompare 和 partition —— 倆個元素應(yīng)該以怎么樣的順序進行排列
→ index蛾洛,first 和 contains ——元素是否符合某個條件
→ min 和 max——兩個元素中的最小/最大值是哪個
→ elementsEqual 和 starts——倆個元素是否相等
→ split——這個元素是否是一個分割符
所有這些函數(shù)的目的都是為了擺脫代碼中那些雜亂無用的部分,比如像是創(chuàng)建新數(shù)組端考,對源數(shù)據(jù)進行for循環(huán)之類的事情雅潭。這些雜亂代碼都被一個單獨的單詞替代了。這可以重點突出那些程序員想要表達的真正重要的邏輯代碼却特。
這些函數(shù)中有一些擁有默認行為扶供。除非你進行過指定,否則sort默認將會把可以做比較的元素按照升序排列裂明。contains對于可以判斷的元素椿浓,會直接檢查倆個元素是否相等。這些行為讓代碼變得更加易讀。升序排列非常自然扳碍,因此array.sort()的意義也很符合直覺提岔。而對于array.index(of:"foo")
這樣的表達方式,也要比array.index { $0 == "foo" }
更容易理解笋敞。
不過在上面的例子中碱蒙,它們都只是特殊情況下的簡寫,集合中的元素并不一定需要可以作比較夯巷,也不一定需要可以判等赛惩。你可以不對整個元素進行操作,比如趁餐,對一個包含人的數(shù)組喷兼,你可以通過他們的年齡進行排序(people.sort{ $0.age<$1.age}
),或者是檢查集合中有沒有包含未成年人(people.sort{ $0.age < 18}
)。你也可以對轉(zhuǎn)變后的元素進行比較后雷,比如通過people.sort { $0.name.uppercased() < $1.name.uppercased() }
來進行忽略大小寫的排序季惯,雖然這么做的效率不會很高。
還有一些其他類似的很有用的函數(shù)臀突,可以接受一個閉包來指定行為勉抓。雖然他們并不存在于標(biāo)準(zhǔn)庫中,但是你可以很容易地自己定義和實現(xiàn)它們惧辈,我們也建議你自己嘗試著做做看:
→ accumulate——累加琳状,和reduce 類似,不過是將所有元素合并到一個數(shù)組中盒齿,而且保留合并時每一步的值念逞。
→ all (matching:) none(matching:) ——測試序列中是不是所有元素都滿足某個標(biāo)準(zhǔn),以及是不是沒有任何元素滿足某個標(biāo)準(zhǔn)边翁。它們可以通過contains和它進行了精心對應(yīng)的否定形式來構(gòu)建翎承。
→ count(where:) —— 計算滿足條件的元素的個數(shù),和filter相似符匾,但是不會構(gòu)建數(shù)組叨咖。
→ indices(where:)——返回一個包含滿足某個標(biāo)準(zhǔn)的所有元素的索引的列表,和index(where:)類似啊胶,但是不會再遇到首個元素時就停止甸各。
index(where:)
→ prefix(while:)——當(dāng)判斷為真的時候,將元素濾出道結(jié)果中焰坪。一旦不為真趣倾,就將剩余的拋棄。和filter類似某饰,但是會提前退出儒恋。這個函數(shù)在處理無序列或者延遲計算(lazily-computed) 的序列時會非常有用善绎。
→ drop(while:)—— 當(dāng)判斷為真的時候,丟棄元素诫尽。一旦不為真禀酱,返回將其余的元素。和prefix(while:) 類似牧嫉,不過返回相反的集合
有時候你可能發(fā)現(xiàn)你寫好了多次同樣模式的代碼剂跟,比如想要在一個逆序數(shù)組中尋找第一個滿足特定條件的元素:
let names = ["Paula", "Elena", "Zoe"]
var lastNameEndingInA: String?
for name in names.reversed() where name.hasSuffix("a") {
lastNameEndingInA = name
break
}
lastNameEndingInA // Optional("Elena")
在這種情況下,你可以考慮為Sequence添加一個小擴展驹止,來將這個邏輯封裝到last(where:)方法中浩聋。我們使用閉包來對for循環(huán)發(fā)生的變化進行抽象描述:
extension Sequence {
func last(where predicate: (Iterator.Element) -> Bool) -> Iterator.Element? {
for element in reversed() where predicate(element) {
return element
}
return nil
}
}
現(xiàn)在我們就能把代碼中的for循環(huán)換成findElement了:
let match = names.last{ $0.hasSuffix("a")}
match// Optional("Elena")
這么做的好處和我們在介紹map時所描述的是一樣的观蜗,相較for循環(huán)臊恋,last(where:)的版本顯然更加易讀。雖然for循環(huán)也很簡單墓捻,但是在你的頭腦里你始終還是要去做個循環(huán)抖仅,這加重了理解的負擔(dān)。使用last(where:)可以減少出錯的可能性砖第,而且它允許你使用let而不是var來聲明變量撤卢。
它和guard
一起也能很好地工作,可能你會想要在元素沒被找到的情況下提早結(jié)束代碼:
guard let match = someSequence.last(where:{$0.passesTest()})
else { return }
可變和帶有狀態(tài)的閉包
當(dāng)遍歷一個數(shù)組的時候梧兼,你可以使用map來執(zhí)行一些其他操作(比如將元素插入到一個查找表中)放吩。我們不催件這么做,來看看下面這個例子:
array.map { item in
table.insert(item)
}
這將副作用(改變了查找表)隱藏在了一個看起來只是對數(shù)組變形的操作中羽杰。在上面這樣的例子中渡紫,使用簡單的for循環(huán)顯然比使用map這樣的函數(shù)更好的選擇。我們有一個叫做forEach的函數(shù)考赛,看起來很符合我們的需求惕澎,但是forEach本身存在一些問題,我們一會詳細討論颜骤。
這種做法和故意給閉包一個局部狀態(tài)有本質(zhì)的不同唧喉。閉包是指那些可以捕獲自身作用域之外的變量的函數(shù),閉包在結(jié)合上高階函數(shù)忍抽,將成為強大的工具八孝。舉個例子,剛才我提到的accumulate函數(shù)可以用map結(jié)婚一個帶有狀態(tài)的閉包來進行實現(xiàn):
extension Array {
func accumulate<Result>(_ initialResult: Result, _ nextPartialResult: (Result, Element) -> Result) -> [Result] {
var running = initialResult
return map { next in
running = nextPartialResult(running, next)
return running
}
}
}
這個函數(shù)創(chuàng)建了一個中間變量來存儲每一步的值鸠项,然后使用map來從這個中間值逐步創(chuàng)建結(jié)果數(shù)組:
[1,2,3,4]. accumulate(0, +) // [1, 3, 6, 10]
要注意的是干跛,這段代碼假設(shè)了變形函數(shù)是以序列原有的順序執(zhí)行的。在我們上面的map中锈锤,事實確實如此驯鳖。但是也有可能對于序列的變形是無序的闲询,比如我們可以有并行處理的元素變形的實現(xiàn)。現(xiàn)在標(biāo)準(zhǔn)庫中的map版本沒有指定它是否會按順序來處理序列浅辙,不過看起來現(xiàn)在這么做是安全的
filter
另一個常見操作是檢查一個數(shù)組扭弧,然后將這個數(shù)組中符合一定條件的元素過濾出來并用它們創(chuàng)建一個新的數(shù)組。對數(shù)組進行循環(huán)并且根據(jù)條件過濾其中元素的模式可以用數(shù)組的filter方法表示:
let nums = [1,2,3,4,5,6,7,8,9,10]
let result = nums.filter{ num in num%2==0}
result//[2,4, 6, 8, 10]
我們可以使用 $0
用來代表參數(shù)的簡寫记舆,這樣代碼將會更加簡短鸽捻。我們可以不用寫出num參數(shù),而上面的代碼重寫為:
let nums = [1,2,3,4,5,6,7,8,9,10]
let result = nums.filter{ $0 % 2 == 0 }
result//[2,4, 6, 8, 10]
對于很短的閉包來說泽腮,這樣做有助于提高可讀性御蒲。但是如果閉包比較復(fù)雜的話,更好的做法應(yīng)該是就像我們之前那個诊赊,顯式地把參數(shù)名字寫出來厚满。不過這更多的是一種個人的選擇,使用一眼看上去更易讀的版本就好碧磅。一個不錯的原則是碘箍,如果閉包可以很好地卸載一行里的話,那么使用簡寫名會更合適鲸郊。
通過組合使用map和filter丰榴,我們可以輕易完成很多數(shù)組操作,而不需要引入中間數(shù)組秆撮。這會使得最終的代碼變得更短更易讀四濒。比如尋找100以內(nèi)同事滿足是偶數(shù)并且是其他數(shù)字的平方的數(shù),我們可以對0..<10進行map來得到所有平方數(shù)职辨,然后再用filter過濾出其中的偶數(shù):
(1..<10).map{ $0 * $0 }.filter{ $0 % 2 == 0 }
// [4, 16, 36, 64]
filter的實現(xiàn)看起來和map很類似:
extension Array {
func filter(_ isIncluded:(Element) -> Bool) -> [Element] {
var result: [Element] = []
for x in self where isIncluded(x){
result.append(x)
}
return result
}
}
一個關(guān)于性能的小提示:如果你正在寫下面這樣的代碼盗蟆,請不要這么做
bigArray.filter { someCondition }.count>0
filter會創(chuàng)建一個全新的數(shù)組,并且會對數(shù)組中的每個元素都進行操作拨匆。然而在上面這段代碼中姆涩,這顯然是不必要的。上面的代碼僅僅檢查了是否有至少一個元素滿足條件惭每,在這個情景下骨饿,使用contains(where:)
更為合適:
bigArray.contains { someCondition }
這種做法會比原來快得多,主要因為倆個方面:它不會去為了計數(shù)而創(chuàng)建一整個全新的數(shù)組台腥,并且一旦匹配了第一個元素宏赘,它就將提前退出。一般來說黎侈,你只應(yīng)該在需要所有結(jié)果時才會去選擇使用filter察署。
有時候你會發(fā)現(xiàn)你想用contains完成一些操作,但是寫出來的代碼很糟糕峻汉。比如贴汪,要是你想檢測一個序列中的所有元素是否全部滿足某個條件脐往,你可以用!sequence.contains { !condition }
,其實你可以用一個更具有描述性名字的新函數(shù)將它封裝起來:
extension Sequence {
public func all ( matching predicate: (Iterator.Element) -> Bool) -> Bool {
// 對于一個條件,如果沒有元素不滿足它的話扳埂,那意味著所有元素都滿足它:
return !contains { !predicate($0) }
}
}
let evenNums=nums.??lter{$0%2==0}//[2,4, 6, 8, 10]
evenNums.all{$0%2==0}//true
Reduce
map和filter都作用在一個數(shù)組上业簿,并產(chǎn)生另一個新的、經(jīng)過修改的數(shù)組阳懂。不過有時候梅尤,你可能會想把所有元素合并為一個新的值。比如要是我們想將元素的值全部加起來岩调∠镌铮可以這樣寫:
var total = 0
let fibs = [1,2,3,4,5,6,7,8,9,10]
for num in fibs {
total = total + num
}
reduce方法對應(yīng)這種模式,它把一個初始值(在這里是0)以及一個將中間值(total)與序列中的元素(num)進行合并的函數(shù)進行了抽象号枕。使用reduce缰揪,我們可以將上面的例子重寫為這樣:
let sum = fibs.reduce(0){ total, num in total + num }
運算符也是函數(shù),所以我們也可以把上面的例子寫成這樣子:
let sum = fibs.reduce(0, +) // 12
reduce的輸出值的類型可以和輸入的類型不同堕澄。舉個例子邀跃,我們可以將一個整數(shù)的列表轉(zhuǎn)換為一個字符創(chuàng),這個字符串中每個數(shù)字后面跟一個空格:
fibs.reduce("") { str, num in str + "\(num) " }
ruduce的實現(xiàn)是這樣的:
extension Array{
func reduce<Result>(_ initialResult:Result, _ nextPartialResult:(Result, Element) -> Result) -> Result {
var result = initalResult
for x in self {
result = nextPartialResult(result,x)
}
return result
}
}
另一個關(guān)于性能的小提示:reduce相當(dāng)靈活蛙紫,所以在構(gòu)建數(shù)組或者是執(zhí)行其他操作時看到reduce的話不足為奇。比如途戒,你可以只使用reduce就能實現(xiàn)map和filter:
extension Array {
func map2<T>(_ transform: (Element) -> T) -> [T] {
return reduce([]) {
$0 + [transform($1)]
}
}
func filter2 (_ isIncluded: (Element) -> Bool) -> [Element] {
return reduce([]) {
isIncluded($1) ? $0 + [$1] : $0
}
}
}
這樣的實現(xiàn)符合美學(xué)坑傅,并且不再需要哪些啰嗦的命令式的for循環(huán)。但是Swift不是Haskell喷斋,Swift的數(shù)組不是列表(list)唁毒。在這里,每次執(zhí)行combine函數(shù)都會通過在前面的元素之后附加一個變換元素或者是已包含的元素星爪,并創(chuàng)建一個全新的數(shù)組浆西。這意味著上面?zhèn)z個實現(xiàn)的復(fù)雜度是O(n^2),而不是O(n)顽腾,隨著數(shù)組長度的增加近零,執(zhí)行這些函數(shù)所消耗的時間將以平方關(guān)系增加。
flatMap
有時候我們會想要對一個數(shù)組用一個函數(shù)進行map抄肖,但是這個變形函數(shù)返回的是另一個數(shù)組而不是單獨的元素久信。
舉個例子,加入我們有一個叫extractLinks的函數(shù)漓摩,它會讀取一個Markdown文件裙士,并返回一個包含該文件中所有連接的URL的數(shù)組。這個函數(shù)的類型是這樣的:
func extractLinks(markdownFile: String) -> [URL]
如果我們有一系列的Markdown文件管毙,并且想將這些文件中所有的鏈接都提取到一個單獨的數(shù)組中的話腿椎,我們可以嘗試使用markdownFiles.map(extractLinks) 來構(gòu)建桌硫。不過問題是這個方法返回的是一個包含了URL的數(shù)組的數(shù)組,這個數(shù)組中的每個元素都是一個文件中的URL的數(shù)組啃炸。為了得到一個包含所有URL的數(shù)組鞍泉,你還要對這個由map取回的數(shù)組中的每個數(shù)組用joined來進行展平(flatten),將它歸并到一個單一數(shù)組中去:
let markdownFiles:[String] = //...
let nestedLinks = markdownFiles.map(extractLinks)
let links = nestedLinks.joined()
flatMap將這倆個操作合并為一個步驟肮帐。markdownFiles.flatMap(links)
將直接把所有Markdown文件中的所有URL放到一個單獨的數(shù)組里并返回咖驮。
flatMap的實現(xiàn)看起來也和map基本一致,不過flatMap需要的是一個能夠返回數(shù)組的函數(shù)作為變換參數(shù)训枢。另外托修,在附加結(jié)果的時候,它使用的是 append(contentsOf:)而不是append(_:)恒界,這樣它能把結(jié)果展平:
extension Array{
func flatMap<T>(_ transform:(Element) -> [T]) -> [T] {
var result: [T] = []
for x in self {
result.append(contentsOf: transform(x))
}
}
}
flatMap的另一個常見使用情景是將不同數(shù)組里面的元素進行合并睦刃。為了得到倆個數(shù)組中的元素的所有配對組合,我們可以對其中一個數(shù)組進行flatMap十酣,然后對另一個進行map操作:
let suits = ["?", "?", "?", "?"]
let ranks = ["J","Q","K","A"]
let result = suits.flatMap { suit in
ranks.map { rank in
(suit, rank)
}
}
使用forEach進行迭代
我們最后要討論的操作是forEach涩拙。它和for循環(huán)的作為非常類似:傳入的函數(shù)對序列中的每個元素執(zhí)行一次。和map不同耸采,forEach不返回任何值兴泥。技術(shù)上來說,我們可以不暇思索地將一個for循環(huán)替換為forEach:
for element in [1,2,3] {
print(element)
}
[1,2,3]. forEach { element in
print ( element)
}
這沒什么特別之處虾宇,不過如果你想要對集合中的每個元素都調(diào)用一個函數(shù)的話搓彻,使用forEach會比較合適。你只需要將函數(shù)或者方法直接通過參數(shù)的方式傳遞給forEach就行了嘱朽,這就可以改善代碼的清晰度和準(zhǔn)確性旭贬。比如在一個viewController里你想把一個數(shù)組中的視圖都加到當(dāng)前View上的話,只需要寫theViews.forEach(view.addSubview)就足夠了搪泳。
不過稀轨,for循環(huán)和forEach有些細微的不同,值得我們注意岸军。比如奋刽,當(dāng)一個for循環(huán)中有return語句時,將它重寫為forEach會造成代碼行為上的極大區(qū)別凛膏。讓我們舉個例子杨名,下面的代碼是通過結(jié)合使用帶有條件的where和for循環(huán)完成的:
extension Array where Element:Equatable {
func index(of element:Element) -> Int?{
for idx in self.indices where self[idx] == element {
return idx
}
return nil
}
}
我們不能直接將where語句加入到forEach中,所以我們可能會用filter來重寫這段代碼(實際上這段代碼是錯誤的):
extension Array where Element: Equatable {
func index_foreach(of element: Element) -> Int? {
self.indices.filter { idx in
self[idx] == element
}. forEach { idx in
return idx
}
return nil
}
}
在forEach中的return并不能返回到外部函數(shù)的作用域之外猖毫,它僅僅只是返回到閉包本身之外台谍,這和原來的邏輯就不一樣了。在這種情況下吁断,編譯器會發(fā)現(xiàn)return語句的參數(shù)沒有被使用趁蕊,從而給出警告坞生,我們可以找到問題所在。但我們不應(yīng)該將找到所有這類錯誤的希望寄托在便一起上掷伙。
在思考一下下面這個簡單的例子:
(1..<10).forEach { number in
print(number)
if number > 2 { return }
}
你可能一開始還沒反應(yīng)過來是己,其實這段代碼將會把輸入的數(shù)字全部打印出來。return語句并不會終止循環(huán)任柜,它做的僅僅是從閉包中返回卒废。
在某些情況下,比如上面的addSubview的例子里宙地,forEach可能會更好摔认。它作為一系列鏈?zhǔn)讲僮魇褂脮r可謂使得其所。想象一下宅粥,你在同一個語句中有一系列map和filter的調(diào)用参袱,這時候你想在調(diào)試時打印出操作鏈中間某個步驟的數(shù)組值,插入一個forEach步驟應(yīng)該是最快的選擇秽梅。
不過抹蚀,因為return在其中的行為不太明確,我們建議大多數(shù)情況下不要使用forEach企垦。這種時候环壤,使用常規(guī)的for循環(huán)可能會更好
數(shù)組類型
切片
除了通過單獨的下標(biāo)來訪問數(shù)組中的元素(比如fibs[0]),我們還可以通過下標(biāo)來獲取某個范圍中的元素竹观。比如镐捧,想要得到數(shù)組中除了首個元素的其他元素,我們可以這么做:
let fibs = [0,1,1,2,3,5]
let slice =fibs[1..<fibs.endIndex]
slice // [1, 1, 2, 3, 5]
type(of: slice) // ArraySlice<Int>
它將返回數(shù)組的一個切片(slice)臭增,其中包含了原數(shù)組中從第二個元素到最后一個元素的數(shù)組。得到的結(jié)構(gòu)的類型是ArraySlice竹习,而不是Array誊抛。切片類型只是數(shù)組的一種表示方式,它背后的數(shù)據(jù)仍然是原來的數(shù)組整陌,只不過是用切片的方式來進行表示拗窃。這意味著原來的數(shù)組并不需要被復(fù)制。ArraySlice具有的方法和Array上定義的方法是一致的泌辫,因此你可以把它們當(dāng)做數(shù)組來進行處理随夸。如果你需要將切片轉(zhuǎn)換為數(shù)組的話,你可以通過將切片傳遞給Array的構(gòu)建方法來完成
Array(fibs[1..<fibs.endIndex])// [1, 1, 2, 3, 5]
橋接
Swift數(shù)組可以橋接到 Objective-C中震放。實際上它們也能被用在C代碼里宾毒,不過后面才會涉及到這個問題。因為NSArray只能持有對象殿遂,所以對Swift數(shù)組進行橋接轉(zhuǎn)換時曾經(jīng)有一個限制诈铛,那就是數(shù)組中的元素能被轉(zhuǎn)換為AnyObject乙各。這限制了只有當(dāng)數(shù)組元素是類實例或者是像是Int,Bool幢竹,String這樣的一小部分能自動橋接到Objective-C對應(yīng)類型的值類型時耳峦,Swift數(shù)組才能被橋接。
不過在Swift3中這個限制已經(jīng)不復(fù)存在了焕毫。Objective-C中的id類型現(xiàn)在導(dǎo)入Swift中時變成Any蹲坷,而不再是AnyObject,也就是說邑飒,任意的Swift數(shù)組都可以被橋接為NSArray了循签。NSArray本身仍舊只接受對象,所以幸乒,編譯器和運行時將自動在后臺把不適配的那些值用類來進行包裝懦底。反方向的解包同樣也是自動進行的。
使用統(tǒng)一的橋接當(dāng)時來處理所有Swift類型到Objective-C的橋接工作罕扎,不僅僅使數(shù)組的處理變得容易聚唐,像是字典(dictionary)或者集合(set)這樣的其他集合類型,也能從中受益腔召。除此之外杆查,它還為未來Swift與Objective-C之間互用性的增強帶來了可能。比如臀蛛,現(xiàn)在Swift的值可以橋接到Objective-C的對象亲桦,那么在未來的Swift本班中,一個Swift值類型完全有可能可以去遵守一個被標(biāo)記為
@objc
的協(xié)議