Swift——數組

數組和可變性

在Swift中最常見的集合類型非數組莫屬。數組是一系列相同類型的元素的有序的容器,對于其中每個元素,我們可以使用下標對其直接進行訪問(這又被稱作隨機訪問)。舉個例子,要創建一個數字的數組,我們可以這么寫:

// 斐波那契數列
let fibs = [0,1,1,2,3,5]

要是我們使用像是append(_:)這樣的方法來修改上面定義的數組的話,會得到一個編譯錯誤。這是因為在上面的代碼中數組使用let聲明為常量的。在很多情景下,這是正確的做法,它可以避免我們不小心對數組做出改變。如果我們想按照變量的方式來使用數組,我們需要將它用var來進行定義:

var mutableFibs = [0,1,1,2,3,5]

現在我們就能很容易地為數字添加單個或是一系列元素了:

mutableFibs.append(8) 
mutableFibs.append(contentsOf: [13, 21]) 
mutableFibs // [0, 1, 1, 2, 3, 5, 8, 13, 21]

區別使用varlet可以給我們帶來不少好處。使用let定義的變量因為其具有不變性,因此更有理由被優先使用。當你讀到類似let fibs = ...這樣的聲明時,你可以確定fibs的值將永遠不變,這一點是由編譯器強制保證的。這在你需要通讀代碼的時候會很有幫助。不過,要注意這只針對那些具有值語義的類型。使用let定義的類實例對象(也就是說對于引用類型)時,它保證的是這個引用永遠不會發生變化,你不能在給這個引用賦一個新的值,但是這個引用所指向的對象卻是可以改變的。

數組和標準庫中的所有集合類型一樣,是具有值語義的。當你創建一個新的數組變量并且把一個已經存在的數組復制給他的時候,這個數組的內容會被復制。舉個例子,在下面的代碼中,x將不會被更改:

var x = [1,2,3]
var y = x
y.append(4)
y //[1,2,3,4]
x //[1,2,3]

var y = x語句復制了x,所以在將4添加到y末尾的時候,x并不會發生改變,它的值依然是[1,2,3]。當你把一個數組傳遞給一個函數時,會發生同樣的事情;方法將得到這個數組的一份本地復制,所有對它的改變都不會影響調用者所持有的數組。

對比一下Foundation框架中的NSArray在可變性上的處理方法。NSArray中沒有更改方法,想要更改一下數組,你必須使用NSMutableArray。但是,就算你擁有的是一個不可變的NSArray,但是它的引用特性并不能保證這個數組不會被改變:

let a = NSMutableArray(array: [1,2,3])

//我們不想讓b發生改變
let b: NSArray = a

//但是事實上他依然能夠被a影響并改變
a.insert(4,at: 3)
b//(1,2,3,4)

//正確的方式在賦值時,先手動進行復制:
let c =  NSMutableArray(array: [1,2,3])

//我們不想讓d發生改變
let d = c.copy() as! NSArray
c.insert(4,at: 3)
d//(1,2,3)

在上面的例子中,顯而易見,我們需要進行復制,因為a的聲明畢竟是可變的。但是,當把數組在方法和函數之間來回傳遞的時候,事情可能就不那么明顯了。

而在Swift中,相較于 NSArray 和 NSMutableArray倆種類型,數組只有一種統一的類型,那就是Array。使用var可以將數組定義為可變,但是區別于與NS的數組,當你使用let定義第二個數組,并將第一個數組賦值給它,也可以保證這個新的數組是不會改變的,因為這里沒有共用的引用。

創建如此多的復制有可能造成性能問題,不過實際上Swift標準庫中的所有集合類型都使用了“寫時復制”這一技術,它能夠保證只在必要的時候對數據進行復制。在我們的例子中,直到y.append被調用的之前,x和y都將共享內部的存儲。在結構體和類中我們也將仔細的研究值語義,并告訴你如何為你自己的類型實現寫時復制特性。

數組和可選值

Swift數組提供了你能想到的所有常規操作方法,像是isEmpty或是count。數組也允許直接使用特定的瞎編直接訪問其中的元素,像是fibs[3]。不過要牢記在使用下標回去元素之前,你需要確保索引值沒有超出范圍。比如取索引值為3的元素,你需要保證數組中至少有4個元素。否則,你的程序會崩潰。

這么設計的主要原因是我們可以數組切片。在Swift中,計算一個索引值這種操作是非常罕見的:

→ 想要迭代數組 ?for x in array
→ 想要迭代除了第一個元素以外的數組其余部分? for x in array.dropFirst()
→ 想要迭代除了最后5個元素以外的數組? for x in array.dropLast(5)
→ 想要列舉數組中的元素和對應的下標?for ( num, element) in collection.enumerated()
→想要尋找一個指定元素的位置 if let idx = array.index { someMatchingLogic($0) }
→想要對數組中的所有元素進行變形?array.map{someTransformation($0)}
→想要篩選出符合某個標準的元素? array. filter { someCriteria($0) }

在Swift 3中傳統的C風格的for循環被移除了,這是Swift不鼓勵你去做索引計算的另一個標志。手動計算和使用索引值往往可能帶來很多潛在的bug,所以最好避免這么做。如果這不可以避免的話,我們可以很容易寫一個可重用的通用函數來進行處理,在其中你可以對精心測試后的索引計算進行封裝,我們將在泛型一章里看到這個例子。

但是有些時候你然后不得不使用索引。對于數組索引來說,當你這么做時,你應該已經深思熟慮,對背后的索引計算邏輯進行過認真思考。在這個前提下,如果每次都要對獲取的結果進行解包的話就顯得多余了——因為這意味著你不信任你的代碼。但實際上你是信任你自己的代碼的,所以你可能會選擇將結果進行強制解包,因為你知道這些下標都是有效的,這一方面十分麻煩,另一方面也是一個壞習慣。當強制借唄編程一種習慣后,很可能你會不小心強制解包了本來不應該解包的東西,所以,為了避免這個行為變成習慣,數組根本沒有給你可選值的選項。

無效的下標操作會造成可控的崩潰,有時候這種行為可能會被叫做不安全,但是這只是安全性的一方面。下標操作在內存安全的意義上是完全安全的,標準庫中的集合總是會執行邊界檢查,并禁止那些越界索引對內存的訪問。

其他操作的行為略有不同。first和last屬性返回的是可選值,當數組為空時,他們返回nil。first相當于isEmpty?nil : self[0]。類似地,如果數組為空時,removeLast將會導致崩潰,而popLast將在數組不為空是刪除最后一個元素返回它,在數組為空時,它將不執行任何操作,直接返回nil。你應該根據自己的需要來選取到底使用那個一個:當你將數組當作棧來使用時,你可能總是想要將empty檢查和移除最后元素組合起來使用;而另一方面,如果你已經知道數組一定非空,那再去處理可選值就完全沒有必要。

數組變形

map

對數組中的每個執行轉換操作是一個很常見的任務。每個程序員可能都寫過上百次這樣的代碼:創建一個新的數組,對已有數組中的元素進行循環依次取出其中的元素,對取出的元素進行操作,并把操作的結果加入到新數組的末尾。比如,下面的代碼計算了一個整數數組里的元素的平方:

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數組擁有的map方法,這個方法來自函數式編程的世界。下面的例子使用了map來完成同樣的操作:

let squares = fibs.map { fib in fib * fib }
squares//[0,1,1,4,9,25]

這種版本有三大優勢。首先,他很短。長度短一般意味著錯誤很少,不過更重要的是,它比原來更清晰。所有無關的內容都被移除了,一旦你習慣了map滿天飛的世界,你就會發現map就像是一個信號,一旦你看到它,就會知道即將有一個函數被作用在數組的每個元素上,并返回另一個數組,它將包含所有被轉換后的結果。

其次,squared將由map的結果得到,我們不會再改變它的值,所有也就不再需要用var來進行聲明了,我們可以將其聲明為let。另外,由于數組元素的類型可以從傳遞給map的函數中推斷出來,我們也不在需要為squared顯示的指明類型了。

最后,創造map函數并不難,你只需要把for循環中的代碼模塊部分用一個泛型函數分裝起來就可以了。下面是一種可能的實現方式(在Swift中,它實際上是Sequence的一個擴展,我們將在之后關于編寫泛型算法的章節里面繼續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是數組中包含的元素類型的占位符,T是元素轉換之后的類型的占位符。map函數本身并不關系Element和T究竟是什么,它們可以是任意類型的。T的具體類型將由調用者傳入給map的transform方法的返回值類型來決定。

實際上,這個函數的簽名應該是

func map<T>(_ transform:(Element)->T) -> [T] 

也就是說,對于可能拋出錯誤的變形函數,map將會把錯誤轉發給調用者。我們會在出錯誤處理一章里覆蓋這個細節。在這里,我們選擇去掉錯誤處理的這個修飾,這樣看起來會更簡單一些。如果你感興趣,可以看看GitHub上Swift倉庫的Sequence.map的源碼實現

使用函數將行為參數化

即使你已經很熟悉map了,也請花一點時間來想一想map的代碼。是什么讓它可以如此通用而且有用?

map可以將模板代碼分離出來,這些模板代碼并不會隨著每次調用發生變動,發生變動的是那些功能代碼,也就是如何變換每個元素的邏輯代碼。map函數通過接受調用者所提供的變換函數作為參數來做到這一點。

縱觀標準庫,我們可以發現很多這樣將行為進行參數化的設計模式。標準庫中有不下十多個函數接受調用者傳入的閉包,并將它作為函數執行關鍵步驟:

→ map和flatMap —— 如何對元素進行變換
→filter——元素是否應該被包含在結果中
→ reduce——如何將元素合并到一個總和的值中
→ sequence——序列中下一個元素應該是什么?
→ forEach——對于一個元素,應該執行怎么樣的操作
→ sort,lexicographicCompare 和 partition —— 倆個元素應該以怎么樣的順序進行排列
→ index,first 和 contains ——元素是否符合某個條件
→ min 和 max——兩個元素中的最小/最大值是哪個
→ elementsEqual 和 starts——倆個元素是否相等
→ split——這個元素是否是一個分割符

所有這些函數的目的都是為了擺脫代碼中那些雜亂無用的部分,比如像是創建新數組,對源數據進行for循環之類的事情。這些雜亂代碼都被一個單獨的單詞替代了。這可以重點突出那些程序員想要表達的真正重要的邏輯代碼。

這些函數中有一些擁有默認行為。除非你進行過指定,否則sort默認將會把可以做比較的元素按照升序排列。contains對于可以判斷的元素,會直接檢查倆個元素是否相等。這些行為讓代碼變得更加易讀。升序排列非常自然,因此array.sort()的意義也很符合直覺。而對于array.index(of:"foo")這樣的表達方式,也要比array.index { $0 == "foo" }更容易理解。

不過在上面的例子中,它們都只是特殊情況下的簡寫,集合中的元素并不一定需要可以作比較,也不一定需要可以判等。你可以不對整個元素進行操作,比如,對一個包含人的數組,你可以通過他們的年齡進行排序(people.sort{ $0.age<$1.age}),或者是檢查集合中有沒有包含未成年人(people.sort{ $0.age < 18})。你也可以對轉變后的元素進行比較,比如通過people.sort { $0.name.uppercased() < $1.name.uppercased() }來進行忽略大小寫的排序,雖然這么做的效率不會很高。

還有一些其他類似的很有用的函數,可以接受一個閉包來指定行為。雖然他們并不存在于標準庫中,但是你可以很容易地自己定義和實現它們,我們也建議你自己嘗試著做做看:

→ accumulate——累加,和reduce 類似,不過是將所有元素合并到一個數組中,而且保留合并時每一步的值。
→ all (matching:) none(matching:) ——測試序列中是不是所有元素都滿足某個標準,以及是不是沒有任何元素滿足某個標準。它們可以通過contains和它進行了精心對應的否定形式來構建。
→ count(where:) —— 計算滿足條件的元素的個數,和filter相似,但是不會構建數組。
→ indices(where:)——返回一個包含滿足某個標準的所有元素的索引的列表,和index(where:)類似,但是不會再遇到首個元素時就停止。
index(where:)
→ prefix(while:)——當判斷為真的時候,將元素濾出道結果中。一旦不為真,就將剩余的拋棄。和filter類似,但是會提前退出。這個函數在處理無序列或者延遲計算(lazily-computed) 的序列時會非常有用。
→ drop(while:)—— 當判斷為真的時候,丟棄元素。一旦不為真,返回將其余的元素。和prefix(while:) 類似,不過返回相反的集合

有時候你可能發現你寫好了多次同樣模式的代碼,比如想要在一個逆序數組中尋找第一個滿足特定條件的元素:

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循環發生的變化進行抽象描述:

extension Sequence {
  func last(where predicate: (Iterator.Element) -> Bool) -> Iterator.Element? {
    for element in reversed() where predicate(element) {
       return element
      }
    return nil
  } 
}

現在我們就能把代碼中的for循環換成findElement了:

let match = names.last{ $0.hasSuffix("a")}
match// Optional("Elena")

這么做的好處和我們在介紹map時所描述的是一樣的,相較for循環,last(where:)的版本顯然更加易讀。雖然for循環也很簡單,但是在你的頭腦里你始終還是要去做個循環,這加重了理解的負擔。使用last(where:)可以減少出錯的可能性,而且它允許你使用let而不是var來聲明變量。

它和guard一起也能很好地工作,可能你會想要在元素沒被找到的情況下提早結束代碼:

guard let match = someSequence.last(where:{$0.passesTest()})
  else { return }

可變和帶有狀態的閉包

當遍歷一個數組的時候,你可以使用map來執行一些其他操作(比如將元素插入到一個查找表中)。我們不催件這么做,來看看下面這個例子:

array.map { item in
  table.insert(item)  
}

這將副作用(改變了查找表)隱藏在了一個看起來只是對數組變形的操作中。在上面這樣的例子中,使用簡單的for循環顯然比使用map這樣的函數更好的選擇。我們有一個叫做forEach的函數,看起來很符合我們的需求,但是forEach本身存在一些問題,我們一會詳細討論。

這種做法和故意給閉包一個局部狀態有本質的不同。閉包是指那些可以捕獲自身作用域之外的變量的函數,閉包在結合上高階函數,將成為強大的工具。舉個例子,剛才我提到的accumulate函數可以用map結婚一個帶有狀態的閉包來進行實現:

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
     }
  } 
}

這個函數創建了一個中間變量來存儲每一步的值,然后使用map來從這個中間值逐步創建結果數組:

[1,2,3,4]. accumulate(0, +) // [1, 3, 6, 10]

要注意的是,這段代碼假設了變形函數是以序列原有的順序執行的。在我們上面的map中,事實確實如此。但是也有可能對于序列的變形是無序的,比如我們可以有并行處理的元素變形的實現。現在標準庫中的map版本沒有指定它是否會按順序來處理序列,不過看起來現在這么做是安全的

filter

另一個常見操作是檢查一個數組,然后將這個數組中符合一定條件的元素過濾出來并用它們創建一個新的數組。對數組進行循環并且根據條件過濾其中元素的模式可以用數組的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用來代表參數的簡寫,這樣代碼將會更加簡短。我們可以不用寫出num參數,而上面的代碼重寫為:

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]

對于很短的閉包來說,這樣做有助于提高可讀性。但是如果閉包比較復雜的話,更好的做法應該是就像我們之前那個,顯式地把參數名字寫出來。不過這更多的是一種個人的選擇,使用一眼看上去更易讀的版本就好。一個不錯的原則是,如果閉包可以很好地卸載一行里的話,那么使用簡寫名會更合適。

通過組合使用map和filter,我們可以輕易完成很多數組操作,而不需要引入中間數組。這會使得最終的代碼變得更短更易讀。比如尋找100以內同事滿足是偶數并且是其他數字的平方的數,我們可以對0..<10進行map來得到所有平方數,然后再用filter過濾出其中的偶數:

(1..<10).map{ $0 * $0 }.filter{ $0 % 2 == 0 }
// [4, 16, 36, 64]

filter的實現看起來和map很類似:

extension Array {
  func filter(_ isIncluded:(Element) -> Bool) -> [Element] {
    var result: [Element] = []
    for x in self where isIncluded(x){
      result.append(x)
    }
    return result
  }
 }

一個關于性能的小提示:如果你正在寫下面這樣的代碼,請不要這么做

bigArray.filter { someCondition }.count>0

filter會創建一個全新的數組,并且會對數組中的每個元素都進行操作。然而在上面這段代碼中,這顯然是不必要的。上面的代碼僅僅檢查了是否有至少一個元素滿足條件,在這個情景下,使用contains(where:)更為合適:

bigArray.contains { someCondition }

這種做法會比原來快得多,主要因為倆個方面:它不會去為了計數而創建一整個全新的數組,并且一旦匹配了第一個元素,它就將提前退出。一般來說,你只應該在需要所有結果時才會去選擇使用filter。

有時候你會發現你想用contains完成一些操作,但是寫出來的代碼很糟糕。比如,要是你想檢測一個序列中的所有元素是否全部滿足某個條件,你可以用!sequence.contains { !condition },其實你可以用一個更具有描述性名字的新函數將它封裝起來:


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都作用在一個數組上,并產生另一個新的、經過修改的數組。不過有時候,你可能會想把所有元素合并為一個新的值。比如要是我們想將元素的值全部加起來。可以這樣寫:

var total = 0
let fibs = [1,2,3,4,5,6,7,8,9,10]
for num in fibs {
  total = total + num
}

reduce方法對應這種模式,它把一個初始值(在這里是0)以及一個將中間值(total)與序列中的元素(num)進行合并的函數進行了抽象。使用reduce,我們可以將上面的例子重寫為這樣:

let sum = fibs.reduce(0){ total, num in total + num }

運算符也是函數,所以我們也可以把上面的例子寫成這樣子:

let sum = fibs.reduce(0, +) // 12

reduce的輸出值的類型可以和輸入的類型不同。舉個例子,我們可以將一個整數的列表轉換為一個字符創,這個字符串中每個數字后面跟一個空格:

fibs.reduce("") { str, num in str + "\(num) " }

ruduce的實現是這樣的:

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
  }
}

另一個關于性能的小提示:reduce相當靈活,所以在構建數組或者是執行其他操作時看到reduce的話不足為奇。比如,你可以只使用reduce就能實現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
     }
   }
}

這樣的實現符合美學,并且不再需要哪些啰嗦的命令式的for循環。但是Swift不是Haskell,Swift的數組不是列表(list)。在這里,每次執行combine函數都會通過在前面的元素之后附加一個變換元素或者是已包含的元素,并創建一個全新的數組。這意味著上面倆個實現的復雜度是O(n^2),而不是O(n),隨著數組長度的增加,執行這些函數所消耗的時間將以平方關系增加。

flatMap

有時候我們會想要對一個數組用一個函數進行map,但是這個變形函數返回的是另一個數組而不是單獨的元素。

舉個例子,加入我們有一個叫extractLinks的函數,它會讀取一個Markdown文件,并返回一個包含該文件中所有連接的URL的數組。這個函數的類型是這樣的:

func extractLinks(markdownFile: String) -> [URL]

如果我們有一系列的Markdown文件,并且想將這些文件中所有的鏈接都提取到一個單獨的數組中的話,我們可以嘗試使用markdownFiles.map(extractLinks) 來構建。不過問題是這個方法返回的是一個包含了URL的數組的數組,這個數組中的每個元素都是一個文件中的URL的數組。為了得到一個包含所有URL的數組,你還要對這個由map取回的數組中的每個數組用joined來進行展平(flatten),將它歸并到一個單一數組中去:

let markdownFiles:[String] = //...
let nestedLinks = markdownFiles.map(extractLinks) 
let links = nestedLinks.joined()

flatMap將這倆個操作合并為一個步驟。markdownFiles.flatMap(links)將直接把所有Markdown文件中的所有URL放到一個單獨的數組里并返回。
flatMap的實現看起來也和map基本一致,不過flatMap需要的是一個能夠返回數組的函數作為變換參數。另外,在附加結果的時候,它使用的是 append(contentsOf:)而不是append(_:),這樣它能把結果展平:

extension Array{
  func flatMap<T>(_ transform:(Element) -> [T]) -> [T] {
    var result: [T] = []
    for x in self {
      result.append(contentsOf: transform(x))
    }
  } 
}

flatMap的另一個常見使用情景是將不同數組里面的元素進行合并。為了得到倆個數組中的元素的所有配對組合,我們可以對其中一個數組進行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循環的作為非常類似:傳入的函數對序列中的每個元素執行一次。和map不同,forEach不返回任何值。技術上來說,我們可以不暇思索地將一個for循環替換為forEach:

for element in [1,2,3] {
  print(element)
 }

[1,2,3]. forEach { element in
  print ( element)
 }

這沒什么特別之處,不過如果你想要對集合中的每個元素都調用一個函數的話,使用forEach會比較合適。你只需要將函數或者方法直接通過參數的方式傳遞給forEach就行了,這就可以改善代碼的清晰度和準確性。比如在一個viewController里你想把一個數組中的視圖都加到當前View上的話,只需要寫theViews.forEach(view.addSubview)就足夠了。

不過,for循環和forEach有些細微的不同,值得我們注意。比如,當一個for循環中有return語句時,將它重寫為forEach會造成代碼行為上的極大區別。讓我們舉個例子,下面的代碼是通過結合使用帶有條件的where和for循環完成的:

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并不能返回到外部函數的作用域之外,它僅僅只是返回到閉包本身之外,這和原來的邏輯就不一樣了。在這種情況下,編譯器會發現return語句的參數沒有被使用,從而給出警告,我們可以找到問題所在。但我們不應該將找到所有這類錯誤的希望寄托在便一起上。
在思考一下下面這個簡單的例子:

(1..<10).forEach { number in
  print(number)
  if number > 2 { return }
}

你可能一開始還沒反應過來,其實這段代碼將會把輸入的數字全部打印出來。return語句并不會終止循環,它做的僅僅是從閉包中返回。

在某些情況下,比如上面的addSubview的例子里,forEach可能會更好。它作為一系列鏈式操作使用時可謂使得其所。想象一下,你在同一個語句中有一系列map和filter的調用,這時候你想在調試時打印出操作鏈中間某個步驟的數組值,插入一個forEach步驟應該是最快的選擇。

不過,因為return在其中的行為不太明確,我們建議大多數情況下不要使用forEach。這種時候,使用常規的for循環可能會更好

數組類型

切片

除了通過單獨的下標來訪問數組中的元素(比如fibs[0]),我們還可以通過下標來獲取某個范圍中的元素。比如,想要得到數組中除了首個元素的其他元素,我們可以這么做:

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>

它將返回數組的一個切片(slice),其中包含了原數組中從第二個元素到最后一個元素的數組。得到的結構的類型是ArraySlice,而不是Array。切片類型只是數組的一種表示方式,它背后的數據仍然是原來的數組,只不過是用切片的方式來進行表示。這意味著原來的數組并不需要被復制。ArraySlice具有的方法和Array上定義的方法是一致的,因此你可以把它們當做數組來進行處理。如果你需要將切片轉換為數組的話,你可以通過將切片傳遞給Array的構建方法來完成

Array(fibs[1..<fibs.endIndex])// [1, 1, 2, 3, 5]
數組切片

橋接

Swift數組可以橋接到 Objective-C中。實際上它們也能被用在C代碼里,不過后面才會涉及到這個問題。因為NSArray只能持有對象,所以對Swift數組進行橋接轉換時曾經有一個限制,那就是數組中的元素能被轉換為AnyObject。這限制了只有當數組元素是類實例或者是像是Int,Bool,String這樣的一小部分能自動橋接到Objective-C對應類型的值類型時,Swift數組才能被橋接。

不過在Swift3中這個限制已經不復存在了。Objective-C中的id類型現在導入Swift中時變成Any,而不再是AnyObject,也就是說,任意的Swift數組都可以被橋接為NSArray了。NSArray本身仍舊只接受對象,所以,編譯器和運行時將自動在后臺把不適配的那些值用類來進行包裝。反方向的解包同樣也是自動進行的。

使用統一的橋接當時來處理所有Swift類型到Objective-C的橋接工作,不僅僅使數組的處理變得容易,像是字典(dictionary)或者集合(set)這樣的其他集合類型,也能從中受益。除此之外,它還為未來Swift與Objective-C之間互用性的增強帶來了可能。比如,現在Swift的值可以橋接到Objective-C的對象,那么在未來的Swift本班中,一個Swift值類型完全有可能可以去遵守一個被標記為@objc的協議

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。