數(shù)組和可變性
在Swift中最常見(jiàn)的集合類型非數(shù)組莫屬。數(shù)組是一系列相同類型的元素的有序的容器,對(duì)于其中每個(gè)元素,我們可以使用下標(biāo)對(duì)其直接進(jìn)行訪問(wèn)(這又被稱作隨機(jī)訪問(wèn))。舉個(gè)例子,要?jiǎng)?chuàng)建一個(gè)數(shù)字的數(shù)組,我們可以這么寫(xiě):
// 斐波那契數(shù)列
let fibs = [0,1,1,2,3,5]
要是我們使用像是append(_:)這樣的方法來(lái)修改上面定義的數(shù)組的話,會(huì)得到一個(gè)編譯錯(cuò)誤。這是因?yàn)樵谏厦娴拇a中數(shù)組使用let聲明為常量的。在很多情景下,這是正確的做法,它可以避免我們不小心對(duì)數(shù)組做出改變。如果我們想按照變量的方式來(lái)使用數(shù)組,我們需要將它用var來(lái)進(jìn)行定義:
var mutableFibs = [0,1,1,2,3,5]
現(xiàn)在我們就能很容易地為數(shù)字添加單個(gè)或是一系列元素了:
mutableFibs.append(8)
mutableFibs.append(contentsOf: [13, 21])
mutableFibs // [0, 1, 1, 2, 3, 5, 8, 13, 21]
區(qū)別使用var和let可以給我們帶來(lái)不少好處。使用let定義的變量因?yàn)槠渚哂胁蛔冃?,因此更有理由被?yōu)先使用。當(dāng)你讀到類似let fibs = ...這樣的聲明時(shí),你可以確定fibs的值將永遠(yuǎn)不變,這一點(diǎn)是由編譯器強(qiáng)制保證的。這在你需要通讀代碼的時(shí)候會(huì)很有幫助。不過(guò),要注意這只針對(duì)那些具有值語(yǔ)義的類型。使用let定義的類實(shí)例對(duì)象(也就是說(shuō)對(duì)于引用類型)時(shí),它保證的是這個(gè)引用永遠(yuǎn)不會(huì)發(fā)生變化,你不能在給這個(gè)引用賦一個(gè)新的值,但是這個(gè)引用所指向的對(duì)象卻是可以改變的。
數(shù)組和標(biāo)準(zhǔn)庫(kù)中的所有集合類型一樣,是具有值語(yǔ)義的。當(dāng)你創(chuàng)建一個(gè)新的數(shù)組變量并且把一個(gè)已經(jīng)存在的數(shù)組復(fù)制給他的時(shí)候,這個(gè)數(shù)組的內(nèi)容會(huì)被復(fù)制。舉個(gè)例子,在下面的代碼中,x將不會(huì)被更改:
var x = [1,2,3]
var y = x
y.append(4)
y //[1,2,3,4]
x //[1,2,3]
var y = x語(yǔ)句復(fù)制了x,所以在將4添加到y(tǒng)末尾的時(shí)候,x并不會(huì)發(fā)生改變,它的值依然是[1,2,3]。當(dāng)你把一個(gè)數(shù)組傳遞給一個(gè)函數(shù)時(shí),會(huì)發(fā)生同樣的事情;方法將得到這個(gè)數(shù)組的一份本地復(fù)制,所有對(duì)它的改變都不會(huì)影響調(diào)用者所持有的數(shù)組。
對(duì)比一下Foundation框架中的NSArray在可變性上的處理方法。NSArray中沒(méi)有更改方法,想要更改一下數(shù)組,你必須使用NSMutableArray。但是,就算你擁有的是一個(gè)不可變的NSArray,但是它的引用特性并不能保證這個(gè)數(shù)組不會(huì)被改變:
let a = NSMutableArray(array: [1,2,3])
//我們不想讓b發(fā)生改變
let b: NSArray = a
//但是事實(shí)上他依然能夠被a影響并改變
a.insert(4,at: 3)
b//(1,2,3,4)
//正確的方式在賦值時(shí),先手動(dòng)進(jìn)行復(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)
在上面的例子中,顯而易見(jiàn),我們需要進(jìn)行復(fù)制,因?yàn)閍的聲明畢竟是可變的。但是,當(dāng)把數(shù)組在方法和函數(shù)之間來(lái)回傳遞的時(shí)候,事情可能就不那么明顯了。
而在Swift中,相較于 NSArray 和 NSMutableArray倆種類型,數(shù)組只有一種統(tǒng)一的類型,那就是Array。使用var可以將數(shù)組定義為可變,但是區(qū)別于與NS的數(shù)組,當(dāng)你使用let定義第二個(gè)數(shù)組,并將第一個(gè)數(shù)組賦值給它,也可以保證這個(gè)新的數(shù)組是不會(huì)改變的,因?yàn)檫@里沒(méi)有共用的引用。
創(chuàng)建如此多的復(fù)制有可能造成性能問(wèn)題,不過(guò)實(shí)際上Swift標(biāo)準(zhǔn)庫(kù)中的所有集合類型都使用了“寫(xiě)時(shí)復(fù)制”這一技術(shù),它能夠保證只在必要的時(shí)候?qū)?shù)據(jù)進(jìn)行復(fù)制。在我們的例子中,直到y(tǒng).append被調(diào)用的之前,x和y都將共享內(nèi)部的存儲(chǔ)。在結(jié)構(gòu)體和類中我們也將仔細(xì)的研究值語(yǔ)義,并告訴你如何為你自己的類型實(shí)現(xiàn)寫(xiě)時(shí)復(fù)制特性。
數(shù)組和可選值
Swift數(shù)組提供了你能想到的所有常規(guī)操作方法,像是isEmpty或是count。數(shù)組也允許直接使用特定的瞎編直接訪問(wèn)其中的元素,像是fibs[3]。不過(guò)要牢記在使用下標(biāo)回去元素之前,你需要確保索引值沒(méi)有超出范圍。比如取索引值為3的元素,你需要保證數(shù)組中至少有4個(gè)元素。否則,你的程序會(huì)崩潰。
這么設(shè)計(jì)的主要原因是我們可以數(shù)組切片。在Swift中,計(jì)算一個(gè)索引值這種操作是非常罕見(jiàn)的:
→ 想要迭代數(shù)組 ?for x in array
→ 想要迭代除了第一個(gè)元素以外的數(shù)組其余部分? for x in array.dropFirst()
→ 想要迭代除了最后5個(gè)元素以外的數(shù)組? for x in array.dropLast(5)
→ 想要列舉數(shù)組中的元素和對(duì)應(yīng)的下標(biāo)?for ( num, element) in collection.enumerated()
→想要尋找一個(gè)指定元素的位置 if let idx = array.index { someMatchingLogic($0) }
→想要對(duì)數(shù)組中的所有元素進(jìn)行變形?array.map{someTransformation($0)}
→想要篩選出符合某個(gè)標(biāo)準(zhǔn)的元素? array. filter { someCriteria($0) }
在Swift 3中傳統(tǒng)的C風(fēng)格的for循環(huán)被移除了,這是Swift不鼓勵(lì)你去做索引計(jì)算的另一個(gè)標(biāo)志。手動(dòng)計(jì)算和使用索引值往往可能帶來(lái)很多潛在的bug,所以最好避免這么做。如果這不可以避免的話,我們可以很容易寫(xiě)一個(gè)可重用的通用函數(shù)來(lái)進(jìn)行處理,在其中你可以對(duì)精心測(cè)試后的索引計(jì)算進(jìn)行封裝,我們將在泛型一章里看到這個(gè)例子。
但是有些時(shí)候你然后不得不使用索引。對(duì)于數(shù)組索引來(lái)說(shuō),當(dāng)你這么做時(shí),你應(yīng)該已經(jīng)深思熟慮,對(duì)背后的索引計(jì)算邏輯進(jìn)行過(guò)認(rèn)真思考。在這個(gè)前提下,如果每次都要對(duì)獲取的結(jié)果進(jìn)行解包的話就顯得多余了——因?yàn)檫@意味著你不信任你的代碼。但實(shí)際上你是信任你自己的代碼的,所以你可能會(huì)選擇將結(jié)果進(jìn)行強(qiáng)制解包,因?yàn)槟阒肋@些下標(biāo)都是有效的,這一方面十分麻煩,另一方面也是一個(gè)壞習(xí)慣。當(dāng)強(qiáng)制借唄編程一種習(xí)慣后,很可能你會(huì)不小心強(qiáng)制解包了本來(lái)不應(yīng)該解包的東西,所以,為了避免這個(gè)行為變成習(xí)慣,數(shù)組根本沒(méi)有給你可選值的選項(xiàng)。
無(wú)效的下標(biāo)操作會(huì)造成可控的崩潰,有時(shí)候這種行為可能會(huì)被叫做不安全,但是這只是安全性的一方面。下標(biāo)操作在內(nèi)存安全的意義上是完全安全的,標(biāo)準(zhǔn)庫(kù)中的集合總是會(huì)執(zhí)行邊界檢查,并禁止那些越界索引對(duì)內(nèi)存的訪問(wèn)。
其他操作的行為略有不同。first和last屬性返回的是可選值,當(dāng)數(shù)組為空時(shí),他們返回nil。first相當(dāng)于isEmpty?nil : self[0]。類似地,如果數(shù)組為空時(shí),removeLast將會(huì)導(dǎo)致崩潰,而popLast將在數(shù)組不為空是刪除最后一個(gè)元素返回它,在數(shù)組為空時(shí),它將不執(zhí)行任何操作,直接返回nil。你應(yīng)該根據(jù)自己的需要來(lái)選取到底使用那個(gè)一個(gè):當(dāng)你將數(shù)組當(dāng)作棧來(lái)使用時(shí),你可能總是想要將empty檢查和移除最后元素組合起來(lái)使用;而另一方面,如果你已經(jīng)知道數(shù)組一定非空,那再去處理可選值就完全沒(méi)有必要。
數(shù)組變形
map
對(duì)數(shù)組中的每個(gè)執(zhí)行轉(zhuǎn)換操作是一個(gè)很常見(jiàn)的任務(wù)。每個(gè)程序員可能都寫(xiě)過(guò)上百次這樣的代碼:創(chuàng)建一個(gè)新的數(shù)組,對(duì)已有數(shù)組中的元素進(jìn)行循環(huán)依次取出其中的元素,對(duì)取出的元素進(jìn)行操作,并把操作的結(jié)果加入到新數(shù)組的末尾。比如,下面的代碼計(jì)算了一個(gè)整數(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方法,這個(gè)方法來(lái)自函數(shù)式編程的世界。下面的例子使用了map來(lái)完成同樣的操作:
let squares = fibs.map { fib in fib * fib }
squares//[0,1,1,4,9,25]
這種版本有三大優(yōu)勢(shì)。首先,他很短。長(zhǎng)度短一般意味著錯(cuò)誤很少,不過(guò)更重要的是,它比原來(lái)更清晰。所有無(wú)關(guān)的內(nèi)容都被移除了,一旦你習(xí)慣了map滿天飛的世界,你就會(huì)發(fā)現(xiàn)map就像是一個(gè)信號(hào),一旦你看到它,就會(huì)知道即將有一個(gè)函數(shù)被作用在數(shù)組的每個(gè)元素上,并返回另一個(gè)數(shù)組,它將包含所有被轉(zhuǎn)換后的結(jié)果。
其次,squared將由map的結(jié)果得到,我們不會(huì)再改變它的值,所有也就不再需要用var來(lái)進(jìn)行聲明了,我們可以將其聲明為let。另外,由于數(shù)組元素的類型可以從傳遞給map的函數(shù)中推斷出來(lái),我們也不在需要為squared顯示的指明類型了。
最后,創(chuàng)造map函數(shù)并不難,你只需要把for循環(huán)中的代碼模塊部分用一個(gè)泛型函數(shù)分裝起來(lái)就可以了。下面是一種可能的實(shí)現(xiàn)方式(在Swift中,它實(shí)際上是Sequence的一個(gè)擴(kuò)展,我們將在之后關(guān)于編寫(xiě)泛型算法的章節(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方法的返回值類型來(lái)決定。
實(shí)際上,這個(gè)函數(shù)的簽名應(yīng)該是
func map<T>(_ transform:(Element)->T) -> [T]也就是說(shuō),對(duì)于可能拋出錯(cuò)誤的變形函數(shù),map將會(huì)把錯(cuò)誤轉(zhuǎn)發(fā)給調(diào)用者。我們會(huì)在出錯(cuò)誤處理一章里覆蓋這個(gè)細(xì)節(jié)。在這里,我們選擇去掉錯(cuò)誤處理的這個(gè)修飾,這樣看起來(lái)會(huì)更簡(jiǎn)單一些。如果你感興趣,可以看看GitHub上Swift倉(cāng)庫(kù)的Sequence.map的源碼實(shí)現(xiàn)
使用函數(shù)將行為參數(shù)化
即使你已經(jīng)很熟悉map了,也請(qǐng)花一點(diǎn)時(shí)間來(lái)想一想map的代碼。是什么讓它可以如此通用而且有用?
map可以將模板代碼分離出來(lái),這些模板代碼并不會(huì)隨著每次調(diào)用發(fā)生變動(dòng),發(fā)生變動(dòng)的是那些功能代碼,也就是如何變換每個(gè)元素的邏輯代碼。map函數(shù)通過(guò)接受調(diào)用者所提供的變換函數(shù)作為參數(shù)來(lái)做到這一點(diǎn)。
縱觀標(biāo)準(zhǔn)庫(kù),我們可以發(fā)現(xiàn)很多這樣將行為進(jìn)行參數(shù)化的設(shè)計(jì)模式。標(biāo)準(zhǔn)庫(kù)中有不下十多個(gè)函數(shù)接受調(diào)用者傳入的閉包,并將它作為函數(shù)執(zhí)行關(guān)鍵步驟:
→ map和flatMap —— 如何對(duì)元素進(jìn)行變換
→filter——元素是否應(yīng)該被包含在結(jié)果中
→ reduce——如何將元素合并到一個(gè)總和的值中
→ sequence——序列中下一個(gè)元素應(yīng)該是什么?
→ forEach——對(duì)于一個(gè)元素,應(yīng)該執(zhí)行怎么樣的操作
→ sort,lexicographicCompare 和 partition —— 倆個(gè)元素應(yīng)該以怎么樣的順序進(jìn)行排列
→ index,first 和 contains ——元素是否符合某個(gè)條件
→ min 和 max——兩個(gè)元素中的最小/最大值是哪個(gè)
→ elementsEqual 和 starts——倆個(gè)元素是否相等
→ split——這個(gè)元素是否是一個(gè)分割符
所有這些函數(shù)的目的都是為了擺脫代碼中那些雜亂無(wú)用的部分,比如像是創(chuàng)建新數(shù)組,對(duì)源數(shù)據(jù)進(jìn)行for循環(huán)之類的事情。這些雜亂代碼都被一個(gè)單獨(dú)的單詞替代了。這可以重點(diǎn)突出那些程序員想要表達(dá)的真正重要的邏輯代碼。
這些函數(shù)中有一些擁有默認(rèn)行為。除非你進(jìn)行過(guò)指定,否則sort默認(rèn)將會(huì)把可以做比較的元素按照升序排列。contains對(duì)于可以判斷的元素,會(huì)直接檢查倆個(gè)元素是否相等。這些行為讓代碼變得更加易讀。升序排列非常自然,因此array.sort()的意義也很符合直覺(jué)。而對(duì)于array.index(of:"foo")這樣的表達(dá)方式,也要比array.index { $0 == "foo" }更容易理解。
不過(guò)在上面的例子中,它們都只是特殊情況下的簡(jiǎn)寫(xiě),集合中的元素并不一定需要可以作比較,也不一定需要可以判等。你可以不對(duì)整個(gè)元素進(jìn)行操作,比如,對(duì)一個(gè)包含人的數(shù)組,你可以通過(guò)他們的年齡進(jìn)行排序(people.sort{ $0.age<$1.age}),或者是檢查集合中有沒(méi)有包含未成年人(people.sort{ $0.age < 18})。你也可以對(duì)轉(zhuǎn)變后的元素進(jìn)行比較,比如通過(guò)people.sort { $0.name.uppercased() < $1.name.uppercased() }來(lái)進(jìn)行忽略大小寫(xiě)的排序,雖然這么做的效率不會(huì)很高。
還有一些其他類似的很有用的函數(shù),可以接受一個(gè)閉包來(lái)指定行為。雖然他們并不存在于標(biāo)準(zhǔn)庫(kù)中,但是你可以很容易地自己定義和實(shí)現(xiàn)它們,我們也建議你自己嘗試著做做看:
→ accumulate——累加,和reduce 類似,不過(guò)是將所有元素合并到一個(gè)數(shù)組中,而且保留合并時(shí)每一步的值。
→ all (matching:) none(matching:) ——測(cè)試序列中是不是所有元素都滿足某個(gè)標(biāo)準(zhǔn),以及是不是沒(méi)有任何元素滿足某個(gè)標(biāo)準(zhǔn)。它們可以通過(guò)contains和它進(jìn)行了精心對(duì)應(yīng)的否定形式來(lái)構(gòu)建。
→ count(where:) —— 計(jì)算滿足條件的元素的個(gè)數(shù),和filter相似,但是不會(huì)構(gòu)建數(shù)組。
→ indices(where:)——返回一個(gè)包含滿足某個(gè)標(biāo)準(zhǔn)的所有元素的索引的列表,和index(where:)類似,但是不會(huì)再遇到首個(gè)元素時(shí)就停止。
index(where:)
→ prefix(while:)——當(dāng)判斷為真的時(shí)候,將元素濾出道結(jié)果中。一旦不為真,就將剩余的拋棄。和filter類似,但是會(huì)提前退出。這個(gè)函數(shù)在處理無(wú)序列或者延遲計(jì)算(lazily-computed) 的序列時(shí)會(huì)非常有用。
→ drop(while:)—— 當(dāng)判斷為真的時(shí)候,丟棄元素。一旦不為真,返回將其余的元素。和prefix(while:) 類似,不過(guò)返回相反的集合
有時(shí)候你可能發(fā)現(xiàn)你寫(xiě)好了多次同樣模式的代碼,比如想要在一個(gè)逆序數(shù)組中尋找第一個(gè)滿足特定條件的元素:
let names = ["Paula", "Elena", "Zoe"]
var lastNameEndingInA: String?
for name in names.reversed() where name.hasSuffix("a") {
lastNameEndingInA = name
break
}
lastNameEndingInA // Optional("Elena")
在這種情況下,你可以考慮為Sequence添加一個(gè)小擴(kuò)展,來(lái)將這個(gè)邏輯封裝到last(where:)方法中。我們使用閉包來(lái)對(duì)for循環(huán)發(fā)生的變化進(jìn)行抽象描述:
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")
這么做的好處和我們?cè)诮榻Bmap時(shí)所描述的是一樣的,相較for循環(huán),last(where:)的版本顯然更加易讀。雖然for循環(huán)也很簡(jiǎn)單,但是在你的頭腦里你始終還是要去做個(gè)循環(huán),這加重了理解的負(fù)擔(dān)。使用last(where:)可以減少出錯(cuò)的可能性,而且它允許你使用let而不是var來(lái)聲明變量。
它和guard一起也能很好地工作,可能你會(huì)想要在元素沒(méi)被找到的情況下提早結(jié)束代碼:
guard let match = someSequence.last(where:{$0.passesTest()})
else { return }
可變和帶有狀態(tài)的閉包
當(dāng)遍歷一個(gè)數(shù)組的時(shí)候,你可以使用map來(lái)執(zhí)行一些其他操作(比如將元素插入到一個(gè)查找表中)。我們不催件這么做,來(lái)看看下面這個(gè)例子:
array.map { item in
table.insert(item)
}
這將副作用(改變了查找表)隱藏在了一個(gè)看起來(lái)只是對(duì)數(shù)組變形的操作中。在上面這樣的例子中,使用簡(jiǎn)單的for循環(huán)顯然比使用map這樣的函數(shù)更好的選擇。我們有一個(gè)叫做forEach的函數(shù),看起來(lái)很符合我們的需求,但是forEach本身存在一些問(wèn)題,我們一會(huì)詳細(xì)討論。
這種做法和故意給閉包一個(gè)局部狀態(tài)有本質(zhì)的不同。閉包是指那些可以捕獲自身作用域之外的變量的函數(shù),閉包在結(jié)合上高階函數(shù),將成為強(qiáng)大的工具。舉個(gè)例子,剛才我提到的accumulate函數(shù)可以用map結(jié)婚一個(gè)帶有狀態(tài)的閉包來(lái)進(jìn)行實(shí)現(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
}
}
}
這個(gè)函數(shù)創(chuàng)建了一個(gè)中間變量來(lái)存儲(chǔ)每一步的值,然后使用map來(lái)從這個(gè)中間值逐步創(chuàng)建結(jié)果數(shù)組:
[1,2,3,4]. accumulate(0, +) // [1, 3, 6, 10]
要注意的是,這段代碼假設(shè)了變形函數(shù)是以序列原有的順序執(zhí)行的。在我們上面的map中,事實(shí)確實(shí)如此。但是也有可能對(duì)于序列的變形是無(wú)序的,比如我們可以有并行處理的元素變形的實(shí)現(xiàn)?,F(xiàn)在標(biāo)準(zhǔn)庫(kù)中的map版本沒(méi)有指定它是否會(huì)按順序來(lái)處理序列,不過(guò)看起來(lái)現(xiàn)在這么做是安全的
filter
另一個(gè)常見(jiàn)操作是檢查一個(gè)數(shù)組,然后將這個(gè)數(shù)組中符合一定條件的元素過(guò)濾出來(lái)并用它們創(chuàng)建一個(gè)新的數(shù)組。對(duì)數(shù)組進(jìn)行循環(huán)并且根據(jù)條件過(guò)濾其中元素的模式可以用數(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用來(lái)代表參數(shù)的簡(jiǎn)寫(xiě),這樣代碼將會(huì)更加簡(jiǎn)短。我們可以不用寫(xiě)出num參數(shù),而上面的代碼重寫(xiě)為:
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]
對(duì)于很短的閉包來(lái)說(shuō),這樣做有助于提高可讀性。但是如果閉包比較復(fù)雜的話,更好的做法應(yīng)該是就像我們之前那個(gè),顯式地把參數(shù)名字寫(xiě)出來(lái)。不過(guò)這更多的是一種個(gè)人的選擇,使用一眼看上去更易讀的版本就好。一個(gè)不錯(cuò)的原則是,如果閉包可以很好地卸載一行里的話,那么使用簡(jiǎn)寫(xiě)名會(huì)更合適。
通過(guò)組合使用map和filter,我們可以輕易完成很多數(shù)組操作,而不需要引入中間數(shù)組。這會(huì)使得最終的代碼變得更短更易讀。比如尋找100以內(nèi)同事滿足是偶數(shù)并且是其他數(shù)字的平方的數(shù),我們可以對(duì)0..<10進(jìn)行map來(lái)得到所有平方數(shù),然后再用filter過(guò)濾出其中的偶數(shù):
(1..<10).map{ $0 * $0 }.filter{ $0 % 2 == 0 }
// [4, 16, 36, 64]
filter的實(shí)現(xiàn)看起來(lái)和map很類似:
extension Array {
func filter(_ isIncluded:(Element) -> Bool) -> [Element] {
var result: [Element] = []
for x in self where isIncluded(x){
result.append(x)
}
return result
}
}
一個(gè)關(guān)于性能的小提示:如果你正在寫(xiě)下面這樣的代碼,請(qǐng)不要這么做
bigArray.filter { someCondition }.count>0
filter會(huì)創(chuàng)建一個(gè)全新的數(shù)組,并且會(huì)對(duì)數(shù)組中的每個(gè)元素都進(jìn)行操作。然而在上面這段代碼中,這顯然是不必要的。上面的代碼僅僅檢查了是否有至少一個(gè)元素滿足條件,在這個(gè)情景下,使用contains(where:)更為合適:
bigArray.contains { someCondition }
這種做法會(huì)比原來(lái)快得多,主要因?yàn)閭z個(gè)方面:它不會(huì)去為了計(jì)數(shù)而創(chuàng)建一整個(gè)全新的數(shù)組,并且一旦匹配了第一個(gè)元素,它就將提前退出。一般來(lái)說(shuō),你只應(yīng)該在需要所有結(jié)果時(shí)才會(huì)去選擇使用filter。
有時(shí)候你會(huì)發(fā)現(xiàn)你想用contains完成一些操作,但是寫(xiě)出來(lái)的代碼很糟糕。比如,要是你想檢測(cè)一個(gè)序列中的所有元素是否全部滿足某個(gè)條件,你可以用!sequence.contains { !condition },其實(shí)你可以用一個(gè)更具有描述性名字的新函數(shù)將它封裝起來(lái):
extension Sequence {
public func all ( matching predicate: (Iterator.Element) -> Bool) -> Bool {
// 對(duì)于一個(gè)條件,如果沒(méi)有元素不滿足它的話,那意味著所有元素都滿足它:
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都作用在一個(gè)數(shù)組上,并產(chǎn)生另一個(gè)新的、經(jīng)過(guò)修改的數(shù)組。不過(guò)有時(shí)候,你可能會(huì)想把所有元素合并為一個(gè)新的值。比如要是我們想將元素的值全部加起來(lái)??梢赃@樣寫(xiě):
var total = 0
let fibs = [1,2,3,4,5,6,7,8,9,10]
for num in fibs {
total = total + num
}
reduce方法對(duì)應(yīng)這種模式,它把一個(gè)初始值(在這里是0)以及一個(gè)將中間值(total)與序列中的元素(num)進(jìn)行合并的函數(shù)進(jìn)行了抽象。使用reduce,我們可以將上面的例子重寫(xiě)為這樣:
let sum = fibs.reduce(0){ total, num in total + num }
運(yùn)算符也是函數(shù),所以我們也可以把上面的例子寫(xiě)成這樣子:
let sum = fibs.reduce(0, +) // 12
reduce的輸出值的類型可以和輸入的類型不同。舉個(gè)例子,我們可以將一個(gè)整數(shù)的列表轉(zhuǎn)換為一個(gè)字符創(chuàng),這個(gè)字符串中每個(gè)數(shù)字后面跟一個(gè)空格:
fibs.reduce("") { str, num in str + "\(num) " }
ruduce的實(shí)現(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
}
}
另一個(gè)關(guān)于性能的小提示:reduce相當(dāng)靈活,所以在構(gòu)建數(shù)組或者是執(zhí)行其他操作時(shí)看到reduce的話不足為奇。比如,你可以只使用reduce就能實(shí)現(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
}
}
}
這樣的實(shí)現(xiàn)符合美學(xué),并且不再需要哪些啰嗦的命令式的for循環(huán)。但是Swift不是Haskell,Swift的數(shù)組不是列表(list)。在這里,每次執(zhí)行combine函數(shù)都會(huì)通過(guò)在前面的元素之后附加一個(gè)變換元素或者是已包含的元素,并創(chuàng)建一個(gè)全新的數(shù)組。這意味著上面?zhèn)z個(gè)實(shí)現(xiàn)的復(fù)雜度是O(n^2),而不是O(n),隨著數(shù)組長(zhǎng)度的增加,執(zhí)行這些函數(shù)所消耗的時(shí)間將以平方關(guān)系增加。
flatMap
有時(shí)候我們會(huì)想要對(duì)一個(gè)數(shù)組用一個(gè)函數(shù)進(jìn)行map,但是這個(gè)變形函數(shù)返回的是另一個(gè)數(shù)組而不是單獨(dú)的元素。
舉個(gè)例子,加入我們有一個(gè)叫extractLinks的函數(shù),它會(huì)讀取一個(gè)Markdown文件,并返回一個(gè)包含該文件中所有連接的URL的數(shù)組。這個(gè)函數(shù)的類型是這樣的:
func extractLinks(markdownFile: String) -> [URL]
如果我們有一系列的Markdown文件,并且想將這些文件中所有的鏈接都提取到一個(gè)單獨(dú)的數(shù)組中的話,我們可以嘗試使用markdownFiles.map(extractLinks) 來(lái)構(gòu)建。不過(guò)問(wèn)題是這個(gè)方法返回的是一個(gè)包含了URL的數(shù)組的數(shù)組,這個(gè)數(shù)組中的每個(gè)元素都是一個(gè)文件中的URL的數(shù)組。為了得到一個(gè)包含所有URL的數(shù)組,你還要對(duì)這個(gè)由map取回的數(shù)組中的每個(gè)數(shù)組用joined來(lái)進(jìn)行展平(flatten),將它歸并到一個(gè)單一數(shù)組中去:
let markdownFiles:[String] = //...
let nestedLinks = markdownFiles.map(extractLinks)
let links = nestedLinks.joined()
flatMap將這倆個(gè)操作合并為一個(gè)步驟。markdownFiles.flatMap(links)將直接把所有Markdown文件中的所有URL放到一個(gè)單獨(dú)的數(shù)組里并返回。
flatMap的實(shí)現(xiàn)看起來(lái)也和map基本一致,不過(guò)flatMap需要的是一個(gè)能夠返回?cái)?shù)組的函數(shù)作為變換參數(shù)。另外,在附加結(jié)果的時(shí)候,它使用的是 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的另一個(gè)常見(jiàn)使用情景是將不同數(shù)組里面的元素進(jìn)行合并。為了得到倆個(gè)數(shù)組中的元素的所有配對(duì)組合,我們可以對(duì)其中一個(gè)數(shù)組進(jìn)行flatMap,然后對(duì)另一個(gè)進(jìn)行map操作:
let suits = ["?", "?", "?", "?"]
let ranks = ["J","Q","K","A"]
let result = suits.flatMap { suit in
ranks.map { rank in
(suit, rank)
}
}
使用forEach進(jìn)行迭代
我們最后要討論的操作是forEach。它和for循環(huán)的作為非常類似:傳入的函數(shù)對(duì)序列中的每個(gè)元素執(zhí)行一次。和map不同,forEach不返回任何值。技術(shù)上來(lái)說(shuō),我們可以不暇思索地將一個(gè)for循環(huán)替換為forEach:
for element in [1,2,3] {
print(element)
}
[1,2,3]. forEach { element in
print ( element)
}
這沒(méi)什么特別之處,不過(guò)如果你想要對(duì)集合中的每個(gè)元素都調(diào)用一個(gè)函數(shù)的話,使用forEach會(huì)比較合適。你只需要將函數(shù)或者方法直接通過(guò)參數(shù)的方式傳遞給forEach就行了,這就可以改善代碼的清晰度和準(zhǔn)確性。比如在一個(gè)viewController里你想把一個(gè)數(shù)組中的視圖都加到當(dāng)前View上的話,只需要寫(xiě)theViews.forEach(view.addSubview)就足夠了。
不過(guò),for循環(huán)和forEach有些細(xì)微的不同,值得我們注意。比如,當(dāng)一個(gè)for循環(huán)中有return語(yǔ)句時(shí),將它重寫(xiě)為forEach會(huì)造成代碼行為上的極大區(qū)別。讓我們舉個(gè)例子,下面的代碼是通過(guò)結(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語(yǔ)句加入到forEach中,所以我們可能會(huì)用filter來(lái)重寫(xiě)這段代碼(實(shí)際上這段代碼是錯(cuò)誤的):
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ù)的作用域之外,它僅僅只是返回到閉包本身之外,這和原來(lái)的邏輯就不一樣了。在這種情況下,編譯器會(huì)發(fā)現(xiàn)return語(yǔ)句的參數(shù)沒(méi)有被使用,從而給出警告,我們可以找到問(wèn)題所在。但我們不應(yīng)該將找到所有這類錯(cuò)誤的希望寄托在便一起上。
在思考一下下面這個(gè)簡(jiǎn)單的例子:
(1..<10).forEach { number in
print(number)
if number > 2 { return }
}
你可能一開(kāi)始還沒(méi)反應(yīng)過(guò)來(lái),其實(shí)這段代碼將會(huì)把輸入的數(shù)字全部打印出來(lái)。return語(yǔ)句并不會(huì)終止循環(huán),它做的僅僅是從閉包中返回。
在某些情況下,比如上面的addSubview的例子里,forEach可能會(huì)更好。它作為一系列鏈?zhǔn)讲僮魇褂脮r(shí)可謂使得其所。想象一下,你在同一個(gè)語(yǔ)句中有一系列map和filter的調(diào)用,這時(shí)候你想在調(diào)試時(shí)打印出操作鏈中間某個(gè)步驟的數(shù)組值,插入一個(gè)forEach步驟應(yīng)該是最快的選擇。
不過(guò),因?yàn)閞eturn在其中的行為不太明確,我們建議大多數(shù)情況下不要使用forEach。這種時(shí)候,使用常規(guī)的for循環(huán)可能會(huì)更好
數(shù)組類型
切片
除了通過(guò)單獨(dú)的下標(biāo)來(lái)訪問(wèn)數(shù)組中的元素(比如fibs[0]),我們還可以通過(guò)下標(biāo)來(lái)獲取某個(gè)范圍中的元素。比如,想要得到數(shù)組中除了首個(gè)元素的其他元素,我們可以這么做:
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>
它將返回?cái)?shù)組的一個(gè)切片(slice),其中包含了原數(shù)組中從第二個(gè)元素到最后一個(gè)元素的數(shù)組。得到的結(jié)構(gòu)的類型是ArraySlice,而不是Array。切片類型只是數(shù)組的一種表示方式,它背后的數(shù)據(jù)仍然是原來(lái)的數(shù)組,只不過(guò)是用切片的方式來(lái)進(jìn)行表示。這意味著原來(lái)的數(shù)組并不需要被復(fù)制。ArraySlice具有的方法和Array上定義的方法是一致的,因此你可以把它們當(dāng)做數(shù)組來(lái)進(jìn)行處理。如果你需要將切片轉(zhuǎn)換為數(shù)組的話,你可以通過(guò)將切片傳遞給Array的構(gòu)建方法來(lái)完成
Array(fibs[1..<fibs.endIndex])// [1, 1, 2, 3, 5]

橋接
Swift數(shù)組可以橋接到 Objective-C中。實(shí)際上它們也能被用在C代碼里,不過(guò)后面才會(huì)涉及到這個(gè)問(wèn)題。因?yàn)镹SArray只能持有對(duì)象,所以對(duì)Swift數(shù)組進(jìn)行橋接轉(zhuǎn)換時(shí)曾經(jīng)有一個(gè)限制,那就是數(shù)組中的元素能被轉(zhuǎn)換為AnyObject。這限制了只有當(dāng)數(shù)組元素是類實(shí)例或者是像是Int,Bool,String這樣的一小部分能自動(dòng)橋接到Objective-C對(duì)應(yīng)類型的值類型時(shí),Swift數(shù)組才能被橋接。
不過(guò)在Swift3中這個(gè)限制已經(jīng)不復(fù)存在了。Objective-C中的id類型現(xiàn)在導(dǎo)入Swift中時(shí)變成Any,而不再是AnyObject,也就是說(shuō),任意的Swift數(shù)組都可以被橋接為NSArray了。NSArray本身仍舊只接受對(duì)象,所以,編譯器和運(yùn)行時(shí)將自動(dòng)在后臺(tái)把不適配的那些值用類來(lái)進(jìn)行包裝。反方向的解包同樣也是自動(dòng)進(jìn)行的。
使用統(tǒng)一的橋接當(dāng)時(shí)來(lái)處理所有Swift類型到Objective-C的橋接工作,不僅僅使數(shù)組的處理變得容易,像是字典(dictionary)或者集合(set)這樣的其他集合類型,也能從中受益。除此之外,它還為未來(lái)Swift與Objective-C之間互用性的增強(qiáng)帶來(lái)了可能。比如,現(xiàn)在Swift的值可以橋接到Objective-C的對(duì)象,那么在未來(lái)的Swift本班中,一個(gè)Swift值類型完全有可能可以去遵守一個(gè)被標(biāo)記為
@objc的協(xié)議