Swift - 陣列、字典、集合

weixin_34128411發表於2019-01-14

Swift語言提供 ArraysSetsDictionaries 三種基本的集合型別用來儲存集合資料。陣列(Arrays)是有序資料的集。集合(Sets)是無序無重複資料的集。字典(Dictionaries)是無序的鍵值對的集。

4010043-d65b06eac32dc206.png
CollectionTypes_intro_2x.png

Swift 語言中的 ArraysSetsDictionaries 中儲存的資料值型別必須明確。這意味著我們不能把錯誤的資料型別插入其中。同時這也說明你完全可以對取回值的型別非常放心。

注意: Swift 的 ArraysSetsDictionaries 型別被實現為泛型集合

陣列

陣列是Swift中最普通的集合,陣列是有序的容器,並且容器中的每一個元素都是相同的型別, 我們可以使用下標對其直接進行訪問(又稱為隨機訪問),相同的值可以多次出現在一個陣列的不同位置中。

注意:
SwiftArray 型別被橋接到 Foundation 中的 NSArray 類。更多關於在 FoundationCocoa 中使用 Array 的資訊,參見 Using Swift with Cocoa and Obejective-C(Swift 4.1)使用 Cocoa 資料型別部分。

陣列的簡單語法

public struct Array<Element> { }

寫Swift陣列應該遵循像Array<Element>這樣的形式,其中Element是這個陣列中唯一允許存在的資料型別。我們也可以使用像[Element]這樣的簡單語法。儘管兩種形式在功能上是一樣的,但是推薦[Element]寫法,Element是一個泛型表示可指定任意型別。

建立一個空陣列

我們可以使用構造語法來建立一個由特定資料型別構成的空陣列:

var someInts = [Int]()
print("someInts is of type [Int] with \(someInts.count) items.”)
//列印 "someInts is of type [Int] with 0 items.”

注意,通過建構函式的型別,someInts的值型別被推斷為[Int]

如果程式碼上下文中已經提供了型別資訊,例如一個函式引數或者一個已經定義好型別的常量或者變數,那麼我們可以使用空陣列語句建立一個空陣列,它的寫法很簡單:[](一對空方括號):

someInts.append(3)   //someInts 現在包含一個 Int 值,值為3
someInts =  [] //someInts 現在是空陣列,但是仍然是 [Int] 型別的。

建立一個帶有預設值的陣列

Swift中的Array型別還提供一個可以建立特定大小並且所有資料都被預設的構造方法。我們可以把準備加入新陣列的資料項數量(count)和適當型別的初始值(repeating)傳入陣列建構函式:

var threeDoubles = Array(repeating: 0.0, count: 3)
//threeDoubles 是一種 [Double] 陣列,等價於 [0.0, 0.0, 0.0]

通過兩個陣列相加建立一個陣列

我們可以使用加法操作符(+)來組合兩種已存在的相同型別陣列。新陣列的資料型別會被從兩個陣列的資料型別中推斷出來:

var anotherThreeDoubles = Array(repeating: 2.5, count: 3)
//anotherThreeDoubles 被推斷為 [Double],等價於 [2.5, 2.5, 2.5]
 
var sixDoubles = threeDoubles + anotherThreeDoubles
//sixDoubles 被推斷為 [Double],等價於 [0.0, 0.0, 0.0, 2.5, 2.5, 2.5]

用陣列字面量構造陣列

我們可以使用陣列字面量來進行陣列構造,這是一種用一個或者多個數值構造陣列的簡單方法。陣列字面量是一系列由逗號分割並由方括號包含的數值:形如:[value 1, value 2, value 3]。下面這個例子建立了一個叫做shoppingList並且儲存String的陣列:

var shoppingList: [String] = ["Eggs", "Milk"]

shoppingList已經被構造並且擁有兩個初始項。shoppingList變數被宣告為“字串值型別的陣列“,記作[String]。 因為這個陣列被規定只有String一種資料結構,所以只有String型別可以在其中被存取。 在這裡,shoppingList陣列由兩個String值("Eggs" 和"Milk")構造,並且由陣列字面量定義。

注意:shoppingList陣列被宣告為變數(var關鍵字建立)而不是常量(let建立)是因為以後可能會有更多的資料項被插入其中。

在這個例子中,字面量僅僅包含兩個String值。匹配了該陣列的變數宣告(只能包含String的陣列),所以這個字面量的分配過程可以作為用兩個初始項來構造shoppingList的一種方式。

由於Swift的型別推斷機制,當我們用字面量構造只擁有相同型別值陣列的時候,我們不必把陣列的型別定義清楚。shoppingList的構造也可以這樣寫:

var shoppingList = ["Eggs", "Milk"]

因為所有陣列字面量中的值都是相同的型別,Swift 可以推斷出[String]shoppingList中變數的正確型別。

訪問和修改陣列

我們可以通過陣列的方法和屬性來訪問和修改陣列,或者使用下標語法,可以使用陣列的只讀屬性count來獲取陣列中的資料項數量:

print("The shopping list contains \(shoppingList.count) items.")
// 輸出 "The shopping list contains 2 items."

使用布林屬性isEmpty作為一個縮寫形式去檢查count屬性是否為0:

if shoppingList.isEmpty {
    print("The shopping list is empty.")
} else {
    print("The shopping list is not empty.")
}
// 列印 "The shopping list is not empty."

也可以使用append()方法在陣列後面新增新的資料項:

shoppingList.append("Flour")
// shoppingList 現在有3個資料項

除此之外,使用加法賦值運算子(+=)也可以直接在陣列後面新增一個或多個擁有相同型別的資料項:

shoppingList += ["Baking Powder"]
// shoppingList 現在有四項了
shoppingList += ["Chocolate Spread", "Cheese", "Butter"]
// shoppingList 現在有七項了

可以直接使用下標語法來獲取陣列中的資料項,把我們需要的資料項的索引值放在直接放在陣列名稱的方括號中:

var firstItem = shoppingList[0]// 第一項是 "Eggs"

注意:第一項在陣列中的索引值是0而不是1。 Swift 中的陣列索引總是從零開始。

我們也可以用下標來改變某個已有索引值對應的資料值:

shoppingList[0] = "Six eggs"
// 其中的第一項現在是 "Six eggs" 而不是 “Eggs"

還可以利用下標來一次改變一系列資料值,即使新資料和原有資料的數量是不一樣的。下面的例子把"Chocolate Spread","Cheese",和"Butter"替換為"Bananas"和 "Apples":

shoppingList[4...6] = ["Bananas", "Apples"]// shoppingList 現在有6項

注意:不可以用下標訪問的形式去在陣列尾部新增新項。

陣列的插入

呼叫陣列的insert(_:at:)方法來在某個具體索引值之前新增資料項:

shoppingList.insert("Maple Syrup", at: 0)
// shoppingList 現在有7項, "Maple Syrup" 現在是這個列表中的第一項

這次insert(_:at:)方法呼叫把值為"Maple Syrup"的新資料項插入列表的最開始位置,並且使用0作為索引值。

陣列的刪除

類似的我們可以使用remove(at:)方法來移除陣列中的某一項。這個方法把陣列在特定索引值中儲存的資料項移除並且返回這個被移除的資料項(我們不需要的時候就可以無視它):

shoppingList.remove(at: 0)
// 索引值為0的資料項被移除
// shoppingList現在只有6項,而且不包括Maple Syrup;
// mapleSyrup常量的值等於被移除資料項的值 "Maple Syrup"

注意:如果我們試著對索引越界的資料進行檢索或者設定新值的操作,會引發一個執行期錯誤。

我們可以使用索引值和陣列的count屬性進行比較來在使用某個索引之前先檢驗是否有效。除了當count等於0 時(說明這是個空陣列),最大索引值一直是count - 1,因為陣列都是零起索引。

資料項被移除後陣列中的空出項會被自動填補,所以現在索引值為0的資料項的值再次等於"Six eggs":

firstItem = shoppingList[0]
// firstItem 現在等於"Six eggs"

如果我們只想把陣列中的最後一項移除,可以使用removeLast()方法而不是remove(at:)方法來避免我們需要獲取陣列的count屬性。就像後者一樣,前者也會返回被移除的資料項:

let apples = shoppingList.removeLast()
// 陣列的最後一項被移除了
// shoppingList現在只有5項,不包括Apples
// apples常量的值現在等於 "Apples" 字串

陣列的遍歷

我們可以使用for-in迴圈來遍歷所有陣列中的資料項:

for item in shoppingList {
   print(item)
}
// Six eggs
// Milk
// Flour
// Baking Powder
// Bananas

如果我們同時需要每個資料項的值和索引值,可以使用enumerated()方法來進行陣列遍歷。enumerated()返回一個由每一個資料項索引值和資料值組成的元組。我們可以把這個元組分解成臨時常量或者變數來進行遍歷:

for (index, value) in shoppingList.enumerated() {
   print("Item \(String(index + 1)): \(value)")
}
// Item 1: Six eggs
// Item 2: Milk
// Item 3: Flour
// Item 4: Baking Powder
// Item 5: Bananas

遍歷陣列,但是不包括第一個元素

for x in shoppingList.dropFirst(){
 print(x) //
}
 
/*
 Milk
 Flour
 Baking Powder
 Bananas
 */

遍歷陣列,但是不包括最後一個或者幾個元素

for x in shoppingList.dropLast(){
 print(x)
}
/*
 Six eggs
 Milk
 Flour
 Baking Powder
 */
for x in shoppingList.dropLast(3){
    print(x)
}
/*
 Six eggs
 Milk
 */
陣列的可變性

舉個例子,要建立一個數字的陣列,我們可以這麼寫:

// 斐波拉契數列
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,34])

區別使用var和let可以給我們帶來不少好處,使用let定義的變數因為其具有不變性,因此更有理由被優先使用,當你讀到類似let fib=...這樣的宣告時,你可以確定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])
let b: NSArray = a // 不想讓b發生改變
a.insert(4, at: 3) // 但是事實上b的改變依然能夠被a影響
b // 1,2,3,4

正確的方式是在賦值時,先收到進行復制

let c = NSMutableArray(array: [1,2,3])
let b: NSArray = c.copy() as! NSArray // 不想讓b發生改變
c.insert(4, at: 3) // 1,2,3,4
b // 1,2,3

該例子中顯而易見,我們需要進行復制,因為a的宣告畢竟是可變的,但是當把陣列在方法和函式之間來回傳遞的時候,事情可能就不那麼明顯了。

而在Swift中,相較於NSArrayNSMutableArray兩種型別,陣列只有嚴重統一的型別,那就是Array。使用var可以將陣列定義為可變,但是區別與NS的陣列,當你使用let定義第二個陣列,並將第一個陣列賦值給它,也可以保證這個新的陣列是不會改變的,因為這裡沒有公用的引用

建立如此多的複製有可能造成效能問題,不過實際上Swift標準庫中的所有集合型別都使用了“寫時複製”這一技術,它能夠保證只在必要的時候對資料進行復制。在上面的例子中,y.append被呼叫之前,x和y都將共享內部的儲存

Swift陣列提供了你能想到的所有常規操作,像是isEmpty或是count。陣列也允許直接使用特定的下標直接訪問其中的元素,像是fib[3]。不過要注意:在使用下標獲取元素之前,需要確保索引值沒有超出範圍,否則會導致程式奔潰

陣列變形

對陣列中的每個值指向轉換是一個很常見的任務。常用操作:建立一個新陣列,對已有陣列中的元素進行迴圈依次取出其中元素,對取出的元素進行操作,並把操作的結果加入到新陣列的末尾。比如:下面的程式碼計算了一個整型陣列裡元素的平方

var squared: [Int] = []
for fib in fibs {
    squared.append(fib * fib)
}
squared // 0,1,1,4,9,25

Swift陣列擁有map方法,這個方法來自函數語言程式設計的世界,專門用於對陣列中的元素進行遍歷操作

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

map 方法接受一個閉包作為引數, 然後它會遍歷整個陣列,並對陣列中每一個元素執行閉包中定義的操作。 相當於對陣列中的所有元素做了一個對映,這是非常普遍的操作

let squared = fibs.map { $0 * $0}
squared // 0,1,1,4,9,25

使用map函式的優勢

  • 程式碼很短,程式碼長度短意味著錯誤少,不過更重要的是,它比原來更清晰了。所有無關的內容都被移除了,一旦習慣了map的使用,就會發現map就像一個訊號,一旦看見它,就會知道即將有一個函式被作用在陣列的每一個元素上,並返回一個陣列,它將包含所有被轉換後的結果

  • squared將由map的結果得到,我們不會再改變它的值,所以也就不再需要用var來進行宣告瞭,我們可以將其宣告為let。另外,由於陣列元素的型別可以從傳遞給map的函式中推斷出來,我們也不再需要為squared顯示地指明型別了

  • 建立map函式並不難,只需要把for迴圈模版部分用一個泛型函式封裝起來就可以了,下面是一種可能的實現方式

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函式本身並不關心
ElementT究竟是什麼,它們可以是任意型別。T的具體型別將由呼叫者傳入給maptransform方法的返回值型別來決定。

index函式

找到具體元素的位置,第一次出現的位置

if let index = array.index(where: { element -> Bool in return element == 4 }) {
  print("index = \(index)") //index = 2
}

另外一個例子:

let students = ["Kofi", "Abena", "Peter", "Kweku", "Akosua"]
if let i = students.index(where: { $0.hasPrefix("A") }) {
    print("\(students[i]) starts with 'A'!")
}
// Prints "Abena starts with 'A'!"
Filter

將陣列中符合一定條件的元素過濾出來並用它們建立一個新的陣列。對陣列進行迴圈並且根據條件過濾其中元素的模式。

let cast = ["Vivien", "Marlon", "Kim", "Karl"]
let shortNames = cast.filter { $0.characters.count < 5 }
print(shortNames)
// Prints "["Kim", "Karl"]"

filter內部實現

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

組合map,filter寫出更簡單的表達,如:查詢100以下所有的偶數

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

對於map,filter都是在一個陣列的基礎上產生一個新陣列或者修改陣列,然後有時候,你想組合所有的元素到一個新值。例如:計算所有元素的和,我們能夠使用如下程式碼:

let number = [0,1,1,2,3,4,5]
var total = 0
for num in number {
    total = total + num
}
print("total = \(total)") //total = 16

reduce方法使用了這種模式,並且抽象為兩個部分,一個初始值,一個是函式用於組合中間值和元素值,使用reduce,我們的程式碼如下:

let sum = number.reduce(0){ total, num in total + num}
//因為操作符+也是函式,所以我們可以直接使用+號
let shortSum = fibs.reduce(0, +)
print("sum = \(sum),shortSum = \(shortSum)") //sum = 16,shortSum = 16

注意:輸出的結果型別並不一定跟元素的型別一樣,例如:我們想轉換integerstring

let stringArray = number.reduce(""){str, num in str + "\(num)"}
print("stringArray = \(stringArray)") //stringArray = 0112345

reduce的內部實現

extension Array {
    func reduce<Result>(_ initialResult: Result, _ nextPartialResult: (Result, Element) -> Result) -> Result {
        var result = initialResult
        for x in self {
            result = nextPartialResult(result, x)
        }
        return result
    }
}
flatMap

通過為序列中的每一個元素進行轉換,即執行閉包,返回的陣列是包含序列的結果.

let numbers = [1, 2, 3, 4]
let mapped = numbers.map { Array(repeating: $0, count: $0) }
print("mapped = \(mapped)") //mapped = [[1], [2, 2], [3, 3, 3], [4, 4, 4, 4]]

let flatMapped = numbers.flatMap { Array(repeating: $0, count: $0) }
print("flatMapped = \(flatMapped)") //flatMapped = [1, 2, 2, 3, 3, 3, 4, 4, 4, 4]

flatMap組合元素來自不同的陣列

let suits = ["♠", "♥", "♣", "♦"]
let ranks = ["J","Q","K","A"]
let results = suits.flatMap { suit in
    ranks.map { rank in
        (suit,rank)
    }
}
print("results = \(results)")
//results = [("♠", "J"), ("♠", "Q"), ("♠", "K"), ("♠", "A"), ("♥", "J"), ("♥", "Q"), ("♥", "K"), ("♥", "A"), ("♣", "J"), ("♣", "Q"), ("♣", "K"), ("♣", "A"), ("♦", "J"), ("♦", "Q"), ("♦", "K"), ("♦", "A")]

flatMap內部實現

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

forEach,就像一個迴圈,傳遞一個函式功能用於執行序列中的每一個元素。但是不像mapforEach並不返回任何值。

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

上下對比一下也沒有看到優勢,但是它能夠隨意使用,如果你想為集合執行單一功能的操作,傳遞一個函式名給forEach而不是一個比包表示式,這樣能夠看起來更加簡單和精準的程式碼。例如:在你的檢視控制器之內,你想新增子檢視( subviews)的陣列到主檢視(main view),僅僅只需要 theViews.forEach(view.addSubview)

然而,對於for迴圈和forEach還是存在一些區別的,例如:如果使用for迴圈執行返回一條語句,使用forEach重寫將更好。對於多語句的遍歷,不要使用forEach

陣列的切片

除了通過單獨的下標來訪問陣列的元素(fibs[0]),還可以通過下標來獲取某個範圍中的元素。比如,想要獲取除了首個元素的其他元素

let slice = fibs[1..<fibs.endIndex]
slice // 1,1,2,3,5
type(of: slice) // ArraySlice<Int>

它將返回一個陣列切片(slice),其中包含了願陣列中從第二個元素到最後一個元素的資料。得到的結果型別是ArraySlice而不是Array

切片型別只是陣列的一種表示方式,它背後的資料仍然是原來的陣列,只不過是用切片的方式來進行表示。

字典

字典是一種儲存多個相同型別的值的容器。每個值(value)都關聯唯一的鍵(key),鍵作為字典中的這個值資料的識別符號。和陣列中的資料項不同,字典中的資料項並沒有具體順序。我們在需要通過識別符號(鍵)訪問資料的時候使用字典,這種方法很大程度上和我們在現實世界中使用字典查字義的方法一樣。

字典型別簡化語法

Swift中的字典使用Dictionary<Key, Value>定義,其中Key是字典中鍵的資料型別,Value是字典中對應於這些鍵所儲存值的資料型別。我們也可以用[Key: Value]這樣簡化的形式去建立一個字典型別。雖然這兩種形式功能上相同,但是後者是首選。

注意:一個字典的Key型別必須遵循Hashable協議,就像Set的值型別。

建立一個空字典

我們可以像陣列一樣使用構造語法建立一個擁有確定型別的空字典:

var namesOfIntegers = [Int: String]()
// namesOfIntegers 是一個空的 [Int: String] 字典

這個例子建立了一個[Int: String]型別的空字典來儲存整數的英語命名。它的鍵是Int型,值是String型。如果上下文已經提供了型別資訊,我們可以使用空字典字面量來建立一個空字典,記作[:](中括號中放一個冒號):

namesOfIntegers[16] = "sixteen"
// namesOfIntegers 現在包含一個鍵值對
namesOfIntegers = [:]
// namesOfIntegers 又成為了一個 [Int: String] 型別的空字典

字典的字面量

我們可以使用字典字面量來構造字典,和陣列字面量擁有相似語法。

字典字面量是一種將一個或多個鍵值對寫作Dictionary集合的快捷途徑。一個鍵值對是一個key和一個value的結合體。在字典字面量中,每一個鍵值對的鍵和值都由冒號分割。這些鍵值對構成一個列表,其中這些鍵值對由方括號包含、由逗號分割:

[key1: value1, key2: value2, key3: value3]

下面的例子建立了一個儲存國際機場名稱的字典。在這個字典中鍵是三個字母的國際航空運輸相關程式碼,值是機場名稱:

var airports: [String: String] = ["YYZ": "Toronto Pearson", "DUB": "Dublin"]

airports字典被宣告為一種[String: String]型別,這意味著這個字典的鍵和值都是String型別。

注意:airports字典被宣告為變數(用var關鍵字)而不是常量(let關鍵字)因為可能會有更多的機場資訊會被新增到這個示例字典中。

airports字典使用字典字面量初始化,包含兩個鍵值對。第一對的鍵是YYZ,值是Toronto Pearson。第二對的鍵是DUB,值是Dublin。這個字典語句包含了兩個String: String型別的鍵值對。它們對應airports變數宣告的型別(一個只有String鍵和String值的字典)所以這個字典字面量的任務是構造擁有兩個初始資料項的airport字典。

和陣列一樣,我們在用字典字面量構造字典時,如果它的鍵和值都有各自一致的型別,那麼就不必寫出字典的型別。airports字典也可以用這種簡短方式定義:

var airports = ["YYZ": "Toronto Pearson", "DUB": "Dublin"]

因為這個語句中所有的鍵和值都各自擁有相同的資料型別,Swift 可以推斷出Dictionary<String, String>airports字典的正確型別。

訪問和修改字典

我們可以通過字典的方法和屬性來訪問和修改字典,或者通過使用下標語法。和陣列一樣,我們可以通過字典的只讀屬性count來獲取某個字典的資料項數量:

print("The dictionary of airports contains \(airports.count) items.")
// 列印 "The dictionary of airports contains 2 items."(這個字典有兩個資料項)

使用布林屬性isEmpty作為一個縮寫形式去檢查count屬性是否為0:

if airports.isEmpty {
    print("The airports dictionary is empty.")
} else {
    print("The airports dictionary is not empty.")
}
// 列印 "The airports dictionary is not empty."

我們也可以在字典中使用下標語法來新增新的資料項。可以使用一個恰當型別的鍵作為下標索引,並且分配恰當型別的新值:

airports["LHR"] = "London"
// airports 字典現在有三個資料項

我們也可以使用下標語法來改變特定鍵對應的值:

airports["LHR"] = "London Heathrow"
// "LHR"對應的值 被改為 "London Heathrow

作為另一種下標方法,字典的updateValue(_:forKey:)方法可以設定或者更新特定鍵對應的值。

就像上面所示的下標示例,updateValue(_:forKey:)方法在這個鍵不存在對應值的時候會設定新值或者在存在時更新已存在的值。

和上面的下標方法不同的是updateValue(_:forKey:)這個方法返回更新值之前的原值。這樣使得我們可以檢查更新是否成功。

updateValue(_:forKey:)方法會返回對應值的型別的可選值。舉例來說:對於儲存String值的字典,這個函式會返回一個String?或者“可選 String”型別的值。如果有值存在於更新前,則這個可選值包含了舊值,否則它將會是nil。

if let oldValue = airports.updateValue("Dublin Airport", forKey: "DUB") {
    print("The old value for DUB was \(oldValue).")
}
// 輸出 "The old value for DUB was Dublin."

我們也可以使用下標語法來在字典中檢索特定鍵對應的值。因為有可能請求的鍵沒有對應的值存在,字典的下標訪問會返回對應值的型別的可選值。如果這個字典包含請求鍵所對應的值,下標會返回一個包含這個存在值的可選值,否則將返回nil:

if let airportName = airports["DUB"] {
    print("The name of the airport is \(airportName).")
} else {
    print("That airport is not in the airports dictionary.")
}
// 列印 "The name of the airport is Dublin Airport."

我們還可以使用下標語法來通過給某個鍵的對應值賦值為nil來從字典裡移除一個鍵值對:

airports["APL"] = "Apple Internation"
// "Apple Internation" 不是真的 APL 機場, 刪除它
airports["APL"] = nil
// APL 現在被移除了

此外,removeValue(forKey:)方法也可以用來在字典中移除鍵值對。這個方法在鍵值對存在的情況下會移除該鍵值對並且返回被移除的值或者在沒有值的情況下返回nil:

if let removedValue = airports.removeValue(forKey: "DUB") {
    print("The removed airport's name is \(removedValue).")
} else {
    print("The airports dictionary does not contain a value for DUB.")
}
// prints "The removed airport's name is Dublin Airport."

字典遍歷

我們可以使用for-in迴圈來遍歷某個字典中的鍵值對。每一個字典中的資料項都以(key, value)元組形式返回,並且我們可以使用臨時常量或者變數來分解這些元組:

for (airportCode, airportName) in airports {
    print("\(airportCode): \(airportName)")
}
// YYZ: Toronto Pearson
// LHR: London Heathrow

通過訪問keys或者values屬性,我們也可以遍歷字典的鍵或者值:

for airportCode in airports.keys {
    print("Airport code: \(airportCode)")
}
// Airport code: YYZ
// Airport code: LHR
 
for airportName in airports.values {
    print("Airport name: \(airportName)")
}
// Airport name: Toronto Pearson
// Airport name: London Heathrow

如果我們只是需要使用某個字典的鍵集合或者值集合來作為某個接受Array例項的 API 的引數,可以直接使用keys或者values屬性構造一個新陣列:

let airportCodes = [String](airports.keys)
// airportCodes 是 ["YYZ", "LHR"]
 
let airportNames = [String](airports.values)
// airportNames 是 ["Toronto Pearson", "London Heathrow"]

Swift的字典型別是無序集合型別。為了以特定的順序遍歷字典的鍵或值,可以對字典的keys或values屬性使用sorted()方法。

合併兩個字典

合併兩個字典,用來做合併的字典需要覆蓋重複的鍵

擴充套件Dictionary型別,為它新增一個merge方法,該方法接受帶合併的字典作為引數。我們可以將引數指明為Dictionary型別,不過更好的選擇是用更加通用的泛型方法來進行實現

我們對引數的要求是,它必須是一個序列,這樣我們就可以對其進行列舉遍歷,另外,序列的元素必須是鍵值對,而且它必須接受呼叫的字典的鍵值值擁有相同的型別。對於任意的Sequence,如果它的Iterator.Element(Key,Value)話,它就滿足我們的要求

extension Dictionary {
    mutating func merge<S>(_ other: S)  where S: Sequence,S.Iterator.Element == (key: Key,value: Value) {
        for (k,v) in other {
            self[k] = v
        }
    }
}

再擴充套件一個字典的初始化方法,通過字典建立字典

extension Dictionary {
    init<S: Sequence>(_ sequence: S) where S.Iterator.Element == (key: Key,value: Value) {
        self = [:]
        self.merge(sequence)
    }
}

對字典的values進行對映

extension Dictionary {
    func mapValues<NewValue>(transform: (Value) -> NewValue) ->[Key: NewValue] {
        return Dictionary<Key,NewValue>(map { (key, value) in
            return (key, transform(value))
        })
    }
}
Hashable要求

字典其實是雜湊表,字典通過鍵的hashValue來為每一個鍵指定一個位置,以及它所對應的儲存。這也就是Dictionary要求它的key型別需要遵守Hashable協議的原因。標準庫中所有的基本資料型別都是遵守Hashable協議的,它們包括字串,整數,浮點數以及布林值。不帶關聯值的列舉值型別也會自動遵守Hashable

如果想將自定義的型別用作字典的key,那麼必須手動為自定義型別新增Hashable並滿足它,這需要你實現hashValue屬性。另外,因為Hashable本身是對Equatable的擴充套件,因此,還需要為自定義型別過載==運算子。

實現必須保證雜湊不變原則:兩個同樣的例項,必須擁有同樣的雜湊值,反過來不必為真:兩個雜湊值相同的例項不一定需要相等。不同的雜湊值的數量是有限的,然後很多可以被雜湊的型別(比如:字串)的個數是無窮的。

雜湊可能重複這一特性,意味著Dictionary必須能夠處理雜湊碰撞。優秀的雜湊演算法總是能給出較少的碰撞,這將保持集合的效能特性。理想狀態下,我們希望得到的雜湊值在整個整數範圍內均勻分佈。在極端的例子下,如果你的實現對所有例項返回相同的雜湊值,那麼這個字典的查詢效能下降到O(n)

優秀雜湊演算法的第二個特質是它應該很快,記住,在字典中進行插入,刪除,或者查詢時,這些雜湊值都要被計算。如果你的hashValue實現要消耗太多時間,那麼它很可能會拖慢你的程式,讓你的字典從O(1)特性中得到的好處損失殆盡

下一個能同時做到這些要求的雜湊演算法並不容易,對於一些由本身就是Hashable的資料型別組成的型別來說,將成員的雜湊值進行“異或”運算往往是一個不錯的起點

struct Person {
    var name: String
    var zipCode: Int
    var birthday: Date
}

extension Person: Equatable {
    static func ==(lhs: Person, rhs: Person) -> Bool {
        return lhs.name == rhs.name && lhs.zipCode == rhs.zipCode && lhs.birthday == rhs.birthday
    }
}

extension Person: Hashable {
    var hashValue: Int {
        return name.hashValue ^ zipCode.hashValue ^ birthday.hashValue
    }
}

異或計算方法的一個限制是,這個操作本身是左右對稱的(也就是說a^b == b^a),對於某些資料的雜湊計算,這有時候會造成不必要的碰撞,你可以新增一個位旋轉並混合使用它們來避免這個問題

最後,當你使用不具有值語義的型別(比如可變的物件)作為字典的鍵時,需要特別小心。如果你在將一個物件用做字典的鍵後,改變了它的內容,它的雜湊值/或相等特性往往也會發生改變,這時候你將無法再在字典中找到它,這時字典在錯誤的位置儲存物件,這將導致字典內部儲存錯誤,對於值型別來說,因為字典中的鍵不會和複製的值共用儲存,因此它也不會被從外部改變,所以不存在這個問題

Set

集合(Set)用來儲存相同型別並且沒有確定順序的值。當集合元素順序不重要時或者希望確保每個元素只出現一次時可以使用集合而不是陣列。

注意
Swift 的 Set 型別被橋接到 Foundation 中的 NSSet 類。
關於使用 FoundationCocoaSet 的知識,參見 Using Swift with Cocoa and Obejective-C(Swift 4.1)使用 Cocoa 資料型別部分。

集合型別的雜湊值

一個型別為了儲存在集合中,該型別必須是可雜湊化的--也就是說,該型別必須提供一個方法來計算它的雜湊值。一個雜湊值是Int型別的,相等的物件雜湊值必須相同,比如a==b,因此必須a.hashValue == b.hashValue

Swift 的所有基本型別(比如String,Int,Double和Bool)預設都是可雜湊化的,可以作為集合的值的型別或者字典的鍵的型別。沒有關聯值的列舉成員值(在列舉有講述)預設也是可雜湊化的。

注意:

你可以使用你自定義的型別作為集合的值的型別或者是字典的鍵的型別,但你需要使你的自定義型別符合 Swift標準庫中的Hashable協議。符合Hashable協議的型別需要提供一個型別為Int的可讀屬性hashValue。由型別的hashValue屬性返回的值不需要在同一程式的不同執行週期或者不同程式之間保持相同。

public struct Set<Element> where Element : Hashable {}

public protocol Hashable : Equatable {
    public var hashValue: Int { get }
    public func hash(into hasher: inout Hasher)
}

public protocol Equatable {
    public static func == (lhs: Self, rhs: Self) -> Bool
}

因為Hashable協議符合Equatable協議,所以遵循該協議的型別也必須提供一個"是否相等"運算子(==)的實現。這個Equatable協議要求任何符合==實現的例項間都是一種相等的關係。也就是說,對於a,b,c三個值來說,==的實現必須滿足下面三種情況:

a == a(自反性)
a == b意味著b == a(對稱性)
a == b && b == c意味著a == c(傳遞性)

集合型別語法

Swift中的Set型別被寫為Set<Element>,這裡的Element表示Set中允許儲存的型別,和陣列不同的是,集合沒有等價的簡化形式。

建立一個空的集合

你可以通過構造器語法建立一個特定型別的空集合:

var letters = Set<Character>()
print("letters is of type Set<Character> with \(letters.count) items.")
// 列印 "letters is of type Set<Character> with 0 items."

注意:通過構造器,這裡的letters變數的型別被推斷為Set<Character>

此外,如果上下文提供了型別資訊,比如作為函式的引數或者已知型別的變數或常量,我們可以通過一個空的陣列字面量建立一個空的Set

letters.insert("a")
// letters 現在含有1個 Character 型別的值
letters = []
// letters 現在是一個空的 Set, 但是它依然是 Set<Character> 型別

用陣列字面量建立集合

你可以使用陣列字面量來構造集合,並且可以使用簡化形式寫一個或者多個值作為集合元素。

下面的例子建立一個稱之為favoriteGenres的集合來儲存String型別的值:

var favoriteGenres: Set<String> = ["Rock", "Classical", "Hip hop"]
// favoriteGenres 被構造成含有三個初始值的集合

這個favoriteGenres變數被宣告為一個String值的集合”,寫為Set<String>。由於這個特定的集合含有指定String型別的值,所以它只允許儲存String型別值。這裡的favoriteGenres變數有三個String型別的初始值("Rock","Classical"和"Hip hop"),並以陣列字面量的方式出現。

注意:favoriteGenres被宣告為一個變數(擁有var標示符)而不是一個常量(擁有let標示符),因為它裡面的元素將會在下面的例子中被增加或者移除。

一個Set型別不能從陣列字面量中被單獨推斷出來,因此Set型別必須顯式宣告。然而,由於 Swift的型別推斷功能,如果你想使用一個陣列字面量構造一個Set並且該陣列字面量中的所有元素型別相同,那麼你無須寫出Set的具體型別。favoriteGenres的構造形式可以採用簡化的方式代替:

var favoriteGenres1: Set = ["Rock", "Classical", "Hip hop"]
//由於陣列字面量中的所有元素型別相同,Swift 可以推斷出Set<String>作為favoriteGenres變數的正確型別。

訪問和修改一個集合

你可以通過Set的屬性和方法來訪問和修改Set。為了找出Set中元素的數量,可以使用其只讀屬性count:

print("I have \(favoriteGenres.count) favorite music genres.")
// 列印 "I have 3 favorite music genres."

使用布林屬性isEmpty作為一個縮寫形式去檢查count屬性是否為0:

if favoriteGenres.isEmpty {
    print("As far as music goes, I'm not picky.")
} else {
    print("I have particular music preferences.")
}
// 列印 "I have particular music preferences."

你可以通過呼叫Set的insert(_:)方法來新增一個新元素:

favoriteGenres.insert("Jazz")
// favoriteGenres 現在包含4個元素

你可以通過呼叫Setremove(_:)方法去刪除一個元素,如果該值是該Set的一個元素則刪除該元素並且返回被刪除的元素值,否則如果該Set不包含該值,則返回nil。另外,Set中的所有元素可以通過它的removeAll()方法刪除。

if let removedGenre = favoriteGenres.remove("Rock") {
    print("\(removedGenre)? I'm over it.")
} else {
    print("I never much cared for that.")
}
// 列印 "Rock? I'm over it."

使用contains(_:)方法去檢查Set中是否包含一個特定的值:

if favoriteGenres.contains("Funk") {
    print("I get up on the good foot.")
} else {
    print("It's too funky in here.")
}
// 列印 "It's too funky in here."

遍歷一個集合

你可以在一個for-in迴圈中遍歷一個Set中的所有值。

for genre in favoriteGenres {
    print("\(genre)")
}
// Classical
// Jazz
// Hip hop

SwiftSet型別沒有確定的順序,為了按照特定順序來遍歷一個Set中的值可以使用sorted()方法,它將返回一個有序陣列,這個陣列的元素排列順序由操作符<對元素進行比較的結果來確定.

for genre in favoriteGenres.sorted() {
    print("\(genre)")
}
// prints "Classical"
// prints "Hip hop"
// prints "Jazz

集合操作

你可以高效地完成Set的一些基本操作,比如把兩個集合組合到一起,判斷兩個集合共有元素,或者判斷兩個集合是否全包含,部分包含或者不相交。

基本集合操作

下面的插圖描述了兩個集合-a和b-以及通過陰影部分的區域顯示集合各種操作的結果。

4010043-6e18561bcaabdd2c.png
20170422075031062.png

1 使用intersection(_:)方法根據兩個集合中都包含的值建立的一個新的集合。
2 使用symmetricDifference(_:)方法根據在一個集合中但不在兩個集合中的值建立一個新的集合。
3 使用union(_:)方法根據兩個集合的值建立一個新的集合。
4 使用subtracting(_:)方法根據不在該集合中的值建立一個新的集合。

let oddDigits: Set = [1, 3, 5, 7, 9]
let evenDigits: Set = [0, 2, 4, 6, 8]
let singleDigitPrimeNumbers: Set = [2, 3, 5, 7]
 
oddDigits.union(evenDigits).sorted()
// [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
oddDigits.intersection(evenDigits).sorted()
// []
oddDigits.subtracting(singleDigitPrimeNumbers).sorted()
// [1, 9]
oddDigits.symmetricDifference(singleDigitPrimeNumbers).sorted()
// [1, 2, 9]

集合成員關係和相等

下面的插圖描述了三個集合-a,b和c,以及通過重疊區域表述集合間共享的元素。集合a是集合b的父集合,因為a包含了b中所有的元素,相反的,集合b是集合a的子集合,因為屬於b的元素也被a包含。集合b和集合c彼此不關聯,因為它們之間沒有共同的元素。

4010043-6a028cb31bb9449b.png
20170422075428988.png

1 使用“是否相等”運算子(==)來判斷兩個集合是否包含全部相同的值。
2 使用isSubset(of:)方法來判斷一個集合中的值是否也被包含在另外一個集合中。
3 使用isSuperset(of:)方法來判斷一個集合中包含另一個集合中所有的值。
4 使用isStrictSubset(of:)或者isStrictSuperset(of:)方法來判斷一個集合是否是另外一個集合的子集合或者。父集合並且兩個集合並不相等。
5 使用isDisjoint(with:)方法來判斷兩個集合是否不含有相同的值(是否沒有交集)。

let a: Set = ["1","2","3","4","5","6"]
let b: Set = ["1","2"]
let c: Set = ["7"]
 
b.isSubset(of: a)  // true
a.isSuperset(of: b) // true
a.isDisjoint(with: c) // true

Set擴充套件

集合唯一和有序性

Sequence新增一個擴充套件,用於獲取Sequence中所有唯一的元素。因為我們是很容易將所有的元素放到Set中,並且返回內容,但是這並不穩定,因為Set的順序是未定義的,為了保證輸入元素的順序和唯一性。進行如下擴充套件

extension Sequence where Iterator.Element: Hashable {
    func unique() -> [Iterator.Element] {
        var seen: Set<Iterator.Element> = []
        return filter({
            if seen.contains($0) {
               return false
            }else {
               seen.insert($0)
                return true
            }
        })
    }
}
 
[1,2,3,12,1,3,4,5,6,4,6].unique() // [1, 2, 3, 12, 4, 5, 6]

陣列和集合的對比

  • Set用於無序的唯一物件,Array是有序的並且可以包含重複資料
  • Array迭代速度比Set看,Set搜尋速度比Array快

參考

《Swift進階》
Collection Types

相關文章