4. 聚集型態

陣列Array、集合Set與字典Dictionary這三大資料型態統稱為聚集型態(collection type),專門用來儲存大量的資料。陣列儲存的資料是有序的(可排序),集合與字典都是無序資料且具有唯一性,不會有重複的資料在同一集合與字典中。字典的每筆資料包含了key與value兩部分,且key值不會重複。聚集型態依據宣告的方式決定是否可在執行時期修改內容或是增加與刪除資料。如果以var作為宣告,該聚集型態為可修改,如果以let作為宣告,聚集型態就相當於常數不可修改了。

4.1 陣列

想像一段連續的空間,這個空間被分隔成許多的格子,每個格子都存放了一筆資料,而這些資料的型態都一致,每個格子都有一個編號,編號從0開始每次增加1,一直編號到格子數量減1為止。這一段連續空間就稱為陣列,並且給個名字,例如array,如下圖所示。

陣列中每個格子編號稱為陣列索引值(index),必定從0開始,一次增加1,中間不會斷號,如上圖的0、1、2、3、4。格子中的資料稱為元素(element),如上圖中方塊中央的數字,元素的資料型態必須與陣列宣告時所設定的資料型態一致資料才能放進格子中,例如陣列宣告為整數陣列時,存入陣列中的資料只能是整數。若將上圖的陣列示意圖換成程式碼,如下:

var array = [18, 7, 26, 16, 9]

陣列用中括號表示,每個元素用逗點隔開,由於該陣列array宣告為變數,因此可在執行時期修改內容,包含增加與減少元素。存取陣列使用陣列索引值,例如要取出索引值1的內容,以及將索引值3的內容由16改為20,程式碼如下:

let n = array[1]   // n == 7
array[3] = 20      // array == [18, 7, 26, 20, 9]

除了在中括號中填入陣列索引值取得該索引的內容外,也可以在中括號中填入索引值範圍,傳回該範圍內的陣列資料,例如:

var zoo = ["獅子", "老虎", "大象", "長頸鹿"]
print(zoo[1...3])
// Prints ["老虎", "大象", "長頸鹿"]
print(zoo[1..<3])
// Prints ["老虎", "大象"]
print(zoo[...1])
// Prints ["獅子", "老虎"]
print(zoo[2...])
// Prints ["大象", "長頸鹿"]
print(zoo[..<1])
// Prints ["獅子"]

陣列宣告方式除了上述的宣告同時就初始化外,如果只是純粹宣告但不初始化,這時必須要說明該陣列的資料型態,例如整數陣列或是字串陣列,宣告語法如下:

var array: [Int]

陣列加入元素前一定要先初始化,沒有初始化的陣列無法增加資料,一般來說初始化時給一個空陣列即可,如下:

var array: [Int]
array = []
array.append(18)

~補充說明~
陣列增加一筆資料用appent(_:)函數,稍後會說明陣列還有哪些重要函數可以使用。

還有一種方式是宣告同時就初始化為空陣列,這裡有兩種寫法:第一種寫法,如下面程式碼的第一行,代表明確告訴編譯器該陣列元素型態;第二種寫法,如第二行,透過初始化的內容讓編譯器自行推斷元素型態。

var array: [Int] = []
var anotherArray = [Int]()

多維陣列

產生一個二維陣列,宣告的語法為 [[Type]],例如 [[Int]] 代表一個整數型態的二維陣列。即便是多維陣列,初始化時的空陣列依然使用 [] 表示。加入資料到二維陣列時使用append(_:)函數,一次增加一個維度的資料。存取二維陣列中的某個元素,使用的語法是 [][],中括號裡面放陣列索引值,例如 [2][1] 代表第3列(row)第2行(column)的元素。

var array: [[Int]] = []

array.append([0, 1, 2])
array.append([3, 4, 5])
array.append([6, 7, 8])
/* array ==
[[0, 1, 2],
 [3, 4, 5],
 [6, 7, 8]]
*/

let n = array[2][1]
// n == 7

Array 結構

標準函數庫中的Array也可以用來產生陣列,尤其需要將所有元素都初始化為同一個值的時候,使用Array來產生陣列就很方便。例如產生一個內容全部為0數量為10的陣列,或是產生一個3 x 3且內容為1的二維陣列。

var array = Array(repeating: 0, count: 10)
// [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
var matrix = Array(repeating: Array(repeating: 1, count: 3), count: 3)
/* matrix ==
 [[1, 1, 1],
  [1, 1, 1],
  [1, 1, 1]]
*/

透過Array,還可以將字串中的每一個字元拆開然後組合成字元陣列,例如將 “hello” 轉成 [“h”, “e”, “l”, “l”, “o”],如下:

let s = Array("hello")
// s == ["h", "e", "l", "l", "o"]

在For-In迴圈中提過的迴圈好朋友stride(from:to:by:)與stride(from:through:by:)函數也可以透過Array轉成陣列。

let arr1 = Array(stride(from: 0, to: 10, by: 1))
// arr1 == [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
let arr2 = Array(stride(from: 5, through: -4, by: -2))
// arr2 == [5, 3, 1, -1, -3]

也可以使用範圍運算子「…」或「..<」來產生連續數字的陣列。

let arr1 = Array(0..<10)
// arr1 == [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
let arr2 = Array(0...5)
// arr2 == [0, 1, 2, 3, 4, 5]

其他重要函數

以下列出的函數中有許多函數呼叫時需配合closure語法,請參考第7章Closure。

陣列合併

名稱:  append(_:)、+、+=
說明:  兩個陣列合併成一個,可以使用append(_:)函數,也可以使用符號「+」或「+=」。
範例:  以下三段程式碼最後結果是一樣的。

var a = [1, 2, 3]
var b = [4, 5, 6]
a.append(contentsOf: b)
// a == [1, 2, 3, 4, 5, 6]
var a = [1, 2, 3]
var b = [4, 5, 6]
a = a + b
// a == [1, 2, 3, 4, 5, 6]
var a = [1, 2, 3]
var b = [4, 5, 6]
a += b
// a == [1, 2, 3, 4, 5, 6] 
取得第一個與最後一個元素

名稱:  first、last
說明:  此為唯讀屬性。傳回陣列中的第一個元素或最後一個元素。如果陣列為空陣列時傳回nil,因此first與last的傳回值型態為Optional,取值時必須加上「!」。
範例:

var zoo = ["獅子", "老虎", "大象", "長頸鹿"]
print(zoo.first!)
// Prints "獅子"
print(zoo.last!)
// Prints "長頸鹿"
尾端附加新元素

名稱:  append(_:)
說明:  將新的元素附加到陣列尾端。
範例:

var zoo = ["獅子", "老虎", "大象", "長頸鹿"]
zoo.append("山羊")
// zoo == ["獅子", "老虎", "大象", "長頸鹿", "山羊"]
指定位置插入新元素

名稱:  insert(_:at:)
說明:  參數at為陣列索引值。新增資料到指定索引值的位置,該索引值原本的資料依序往後移動。
範例:

var zoo = ["獅子", "老虎", "大象", "長頸鹿"]
zoo.insert("山羊", at: 0)
// zoo == ["山羊", "獅子", "老虎", "大象", "長頸鹿"]
內容取代

名稱:  replaceSubrange(_:with:)
說明:  使用範圍運算子將陣列中某範圍內的元素用另外一個陣列取代。
範例:  下述程式碼將原本陣列中索引值1…1範圍內的元素換掉,也就是老虎換成山羊與犀牛。

var zoo = ["獅子", "老虎", "大象", "長頸鹿"]
zoo.replaceSubrange(1...1, with: ["山羊", "犀牛"])
// zoo == ["獅子", "山羊", "犀牛", "大象", "長頸鹿"]
刪除特定索引值元素

名稱:  remove(at:)
說明:  刪除並傳回特定陣列索引值的元素,該索引值之後的元素會全部往前移動,若刪除的索引值超過陣列索引值範圍會導致程式當掉。
範例:

var zoo = ["獅子", "老虎", "大象", "長頸鹿"]
zoo.remove(at: 1)
// zoo == ["獅子", "大象", "長頸鹿"]
刪除第一個元素或最後一個元素

名稱:  removeFirst()、removeLast()
說明:  刪除陣列第一個元素或是最後一個元素,並傳回刪除的內容,若陣列為空陣列時執行此函數會導致程式當掉。
範例:

var zoo = ["獅子", "老虎", "大象", "長頸鹿"]
zoo.removeFirst()
// zoo == ["老虎", "大象", "長頸鹿"]
zoo.removeLast()
// zoo == ["老虎", "大象"]
從頭或從尾刪除多個元素

名稱:  removeFirst(_:)、removeLast(_:)
說明:  從陣列開頭處刪除多個元素或從陣列結尾處刪除多個元素。當刪除數量超過陣列大小時會導致程式當掉。
範例:

var zoo = ["獅子", "老虎", "大象", "長頸鹿"]
zoo.removeFirst(2)
// zoo == ["大象", "長頸鹿"]
zoo.removeLast(2)
// zoo == []
刪除範圍內元素

名稱:  removeSubrange(_:)
說明:  使用範圍運算子刪除某範圍內的所有元素。若範圍超過陣列索引值範圍會導致程式當掉。
範例:  刪除陣列索引值1到索引值2的所有元素。

var zoo = ["獅子", "老虎", "大象", "長頸鹿"]
zoo.removeSubrange(1...2)
// zoo == ["獅子", "長頸鹿"]
刪除滿足特定條件元素

名稱:  removeAll(where:)
說明:  透過closure刪除特定條件的元素。
範例:  將元素內容大於等於10的資料刪除。

var numbers = [5, 7, 10, 2, 13]
numbers.removeAll { (n) -> Bool in
    n >= 10
}
// numbers == [5, 7, 2]

Closure可省略成如下寫法,稱為trailing closure。

var numbers = [5, 7, 10, 2, 13]
numbers.removeAll { $0 >= 10 }
傳回第一個滿足條件的元素

名稱:  first(where:)、last(where:)
說明:  從陣列頭或尾開始傳回第一個滿足條件的元素,若無傳回nil。
範例:  陣列內容必須由小到大排序,first函數會傳回大於60的最小數字,last函數會傳回不到60的最大數字。

let grade = [53, 58, 61, 72, 80]
let v1 = grade.first(where: { $0 >= 60 })
let v2 = grade.last(where: { $0 < 60 })
// v1 == 61
// v2 == 58
傳回第一個滿足條件元素的索引值

名稱:  firstIndex(where:)、lastIndex(where:)
說明:  從陣列頭或尾開始傳回第一個滿足條件元素的索引值,若無傳回nil。
範例:  先將陣列內容由小到大排序,firstIndex函數會傳回大於60最小數字的索引值,lastIndex函數會傳回不到60最大數字的索引值。

let grade = [53, 58, 61, 72, 80]
let index1 = grade.firstIndex(where: { $0 >= 60 })
let index2 = grade.lastIndex(where: { $0 < 60 })
// index1 == 2
// index2 == 1
傳回陣列前n個元素,不包含n

名稱:  prefix(_:)、prefix (upTo:)
說明:  若n超過元素個數時這兩個函數有不一樣的結果,prefix(_:)傳回整個陣列,prefix(upTo:)會導致程式當掉。若n未超過元素個數時,這兩個函數功能相同。
範例:

let numbers = [10, 20, 30, 40, 50]
print(numbers.prefix(2))
// Prints [10, 20]
print(numbers.prefix(upTo: 2))
// Prints [10, 20]
print(numbers.prefix(10))
// Prints [10, 20, 30, 40, 50]
print(numbers.prefix(upTo: 10))
// Fatal error: Array index is out of range
傳回陣列前n個元素,包含n

名稱: prefix(through:)
說明:  若n超過元素個數,此函數會導致程式當掉。
範例:

let numbers = [10, 20, 30, 40, 50]
print(numbers.prefix(through: 2))
// Prints [10, 20, 30]
傳回第一個不符合條件前的所有元素

名稱:  prefix(while:)
說明:  傳回陣列從頭開始符合條件的元素直到不符合條件為止。
範例:  先將陣列由小到大排序,然後傳回不到40所在位置以前的所有元素。

let numbers = [10, 20, 30, 40, 50]
print(numbers.prefix(while: { $0 < 40 }))
// Prints [10, 20, 30]
傳回指定索引值開始一直到尾端的所有元素

名稱:  suffix(from:)
說明:  傳回陣列從指定索引值開始一直到尾端的所有元素。
範例:

let numbers = [10, 20, 30, 40, 50]
print(numbers.suffix(from: 1))
// Prints [20, 30, 40, 50]
移除元素並傳回新陣列

名稱:  dropFirst()、dropFirst(_:)、dropLast()、dropLast(_:)
說明:  這四個函數都會傳回一個新陣列,新陣列內容分別為去除第一個元素;去除開頭n個元素;去除最尾端的元素;去除最尾端n個元素。運算完後原有陣列內容不變。
範例:

let numbers = [10, 20, 30, 40, 50]
var new = numbers.dropFirst()
print(new)
// Prints [20, 30, 40, 50]

new = numbers.dropFirst(2)
print(new)
// Prints [30, 40, 50]

new = numbers.dropLast()
print(new)
// Prints [10, 20, 30, 40]

new = numbers.dropLast(2)
print(new)
// Prints [10, 20, 30]
移除滿足條件元素直到未滿足為止

名稱:  drop(while:)
說明:  移除滿足條件元素直到未滿足為止,而第一個未滿足條件元素以及之後所有元素均保留,且不再檢查之後的元素是否滿足條件。運算結果傳回新陣列原有陣列內容不變。
範例:  以下例而言,陣列要先排序,否則只會傳回空陣列。

let numbers = [45, 60, 32, 50, 10, 25]
let sorted = numbers.sorted()
// sorted == [10, 25, 32, 45, 50, 60]
let new = sorted.drop(while: { $0 < 30 })
print(new)
// Prints [32, 45, 50, 60]
取出並刪除尾端元素

名稱:  popLast()
說明:  取出並刪除陣列尾端元素,如果陣列是空陣列時傳回nil。
範例:

var numbers = [5, 7, 10, 2, 13]
let n = numbers.popLast()
// n == 13
// numbers == [5, 7, 10, 2]
排序

名稱:  sort()、sort(_:)、sorted()、sorted(_:)
說明:  排序有四個函數,有ed結尾的代表排序完成後會產生新陣列,原本陣列內容不變,沒有ed結尾的代表排序結果會修改原本陣列內容。不帶參數的代表順向排序,有帶參數的,該參數形式為closure,可用來設計排序方式。能夠排序的元素資料型態必須符合Comparable協定。協定請參考第12章協定。
補充說明:集合與字典雖然也可以排序,但因為這兩個類型本質上是無序的,因此排序結果會以陣列形式傳回,也就是說,集合與字典實際上是先將元素轉成陣列後再排序,所以排序功能主要還是使用於陣列。
範例:

let numbers = [7, 6, 2, 8, 9, 1]
let asc = numbers.sorted()
// asc == [1, 2, 6, 7, 8, 9]
let desc = numbers.sorted(by: >)
// desc == [9, 8, 7, 6, 2, 1]
反轉

名稱:  reverse()、reversed()
說明:  將陣列頭尾對調。有ed結尾的函數會傳回反轉後的陣列,原陣列內容不變,沒有ed結尾的函數反轉完後會修改原本陣列內容。
範例:

let numbers = [1, 2, 3, 4]
let reverse = numbers.reversed()
print(Array(reverse))
// Prints [4, 3, 2, 1]
洗牌

名稱:  shuffle()、shuffled()
說明:  將陣列順序隨機打亂。有ed結尾的函數會傳回打亂後的陣列,原陣列內容不變,沒有ed結尾的函數打亂後會修改原本陣列內容。
範例:

var numbers = [1, 2, 3, 4, 5, 6]
numbers.shuffle()
print(numbers)
// Prints [3, 6, 4, 2, 5, 1]
分割

名稱:  partition(by:)
說明:  此函數會先將陣列排序,然後根據條件傳回特定索引值,之後再根據此索引值將陣列分割成兩部分。
範例:  將成績分割成及格與不及格兩個陣列。

var score = [58, 72, 60, 75, 51, 83, 90]
let p = score.partition(by: { $0 >= 60 })
// score == [58, 51, 60, 75, 72, 83, 90]
// p == 2

let fail = score[..<p]
let pass = score[p...]
// fail == [58, 51]
// pass == [60, 75, 72, 83, 90]
對調

名稱:  swapAt(_:_:)
說明:  根據陣列索引值,將陣列中兩元素位置對調。
範例:

var numbers = [10, 20, 30, 40]
numbers.swapAt(1, 3)
// numbers == [10, 40, 30, 20]
接合

名稱:  joined()、joined(separator:)
說明:  此函數僅能使用於元素型態為String的陣列。joined()將字串陣列中的所有元素接合(concatenate)成單一字串,joined(separator:)在接合時可以插入特定字串。
範例:  

let array = ["a", "b", "c", "d"]
let joined = array.joined()
// joined == "abcd"

範例:

let array = ["a", "b", "c", "d"]
let csv = array.joined(separator: ",")
// csv == "a,b,c,d" 

4.2 集合

集合中的元素沒有順序性,所以不像陣列有陣列索引值,可以決定元素放置的位置,在集合中,元素就只是放進去而已,無法指定放在哪裡。此外,集合中的元素也具有唯一性,重複加入並不會讓已有的元素再增加一個。集合使用的符號與陣列相同,都是中括號,因此在宣告集合變數或常數時必須明確告訴編譯器其為集合型態,否則就會變成陣列。例如:

let set: Set = [1, 2, 3]

使用let宣告的集合為不可變動集合,之後無法再修改、增加或刪除元素。使用var代表此集合為可變動集合,之後可以修改其內容。

集合在宣告的時候如果沒有初始化值,如上述程式碼中的 [1, 2, 3],或是初始化為空集合,這時就必須明確指定集合內的元素型態,例如Set<Int>或Set<String>,代表集合中元素的資料型態為整數型態與字串型態。<Int>或<String>請參考第14章泛型。下面這個例子中,變數set1為整數集合,且由於並未初始化所以將資料加入集合前必須先初始化(第三行程式碼表示初始化為空集合),變數set2為字串集合並且已經初始化,因此可直接加入資料。

var set1: Set<Int>
var set2: Set<String> = []
set1 = []
set1.insert(10)
set1.insert(20)
set2.insert("汽車")
set2.insert("火車")

將資料加到集合中使用insert(_:)函數,因為集合中的元素沒有順序性,因此不像陣列還可以指定插入的位置,在集合中就是把資料放進去就是了。函數insert(_:)的傳回值為(inserted: Bool, memberAfterInsert: Element)的tuple格式,如果inserted為true,代表集合中還沒有要插入的資料,如果為false,代表集合中已經存在此資料了。memberAfterInsert代表這次插入的資料。舉例如下,若集合set2中已經存在汽車,再將汽車插入到此集合中並不會產生錯誤,只是回傳值在inserted部分為false而已。

let (inserted, element) = set2.insert("汽車")
if !inserted {
    print("集合中已經有\(element)")
}

移除集合中的元素,使用remove(_:)函數,參數放的是要刪除的元素內容,不是索引值,如下:

var set: Set = [1, 2, 3, 4]
set.remove(2)
print(set)
// Prints [1, 3, 4]

集合可以使用For-In迴圈將集合中每個元素經歷一遍,如下:

let vehicles: Set = ["汽車", "飛機", "火車", "機車"]
for v in vehicles {
    print("交通工具包含\(v)")
}
// 交通工具包含汽車
// 交通工具包含飛機
// 交通工具包含火車
// 交通工具包含機車

要判斷集合中是否存在某個元素,使用contains(_:)函數,如下:

if vehicles.contains("汽車") {
    print("汽車屬於交通工具")
}

雖然集合中的元素是無序的,但是集合還是提供了排序函數。排序函數會將集合中的元素轉型為陣列,例如在下述程式碼中使用標準函數庫的type(of:)函數來顯示集合排序完後的資料型態,可以發現集合排序後的資料型態是陣列型態。

let set: Set = [5, 2, 3, 4, 7]
let array = set.sorted()
print(type(of: array))
// Prints "Array<Int>"
print(array)
// Prints [2, 3, 4, 5, 7]

雜湊

能夠放進集合的元素其資料型態必須符合Hashable協定,Swift的基本資料型態(String、Int、Double與Bool…等)預設都已經符合Hashable,因此這些型態的值都可以儲存到集合中。自訂的型態只要能夠符合Hashable協定的規範,也可以放進集合。協定請參考第12章協定。

集合運算

集合提供了豐富的集合運算函數,讓我們可以進行交集、聯集、差集…等各種集合運算。假設集合s1與集合s2的內容如下:

let s1: Set = [1, 2, 3]
let s2: Set = [3, 4, 5]
交集

print(s1.intersection(s2))
// Prints [3]
聯集
print(s1.union(s2))
// Prints [1, 2, 3, 4, 5]
差集

差集是集合相減,因此s1.subtracting(s2)與s2.subtracting(s1)結果不一樣。

print(s1.subtracting(s2))
// Prints [1, 2]
print(s2.subtracting(s1))
// Prints [4, 5
對稱差集

有兩種定義,一種是兩個集合先聯集後再減去兩個集合的交集,或是集合1減去集合2再聯集集合2減去集合1。

print(s1.symmetricDifference(s2))
// Prints [1, 2, 4, 5]

~補充說明~
集合運算結果不會排序,上述程式碼列出的是特意排序後的結果,實際執行時只要元素一樣就代表正確了。

集合比較

集合也提供了許多函數用來比較兩個集合,例如兩個集合是否相等,或是否是另外一個集合的子集合…等。如下圖,我們稱s1是s2的完全超集合,s2是s1的完全子集合,s1與s3交集不為空集合,而s2與s3交集為空集合。

相等與不相等

使用「==」以及「!=」來判斷兩個集合的元素是否相等或不相等。

let s1: Set = [1, 2, 3]
let s2: Set = [1, 3, 2]
print(s1 == s2)
// Print "true"
print(s1 != s2)
// Print "false"
子集合

若s2中所有的元素在s1中都找的到,s2稱為s1子集合。

let s1: Set = [1, 2, 3, 4]
let s2: Set = [1, 2, 4]
print(s2.isSubset(of: s1))
// Print "true"
完全子集合

若s2中所有的元素在s1中都找的到,且s1不等於s2,s2稱為s1的完全子集合。例如s2與s3相等,所以s3是s2的子集合,但不是完全子集合。

let s1: Set = [1, 2, 3, 4]
let s2: Set = [1, 2, 4]
let s3: Set = [1, 2, 4]
print(s2.isStrictSubset(of: s1))
// Prints "true"

print(s3.isSubset(of: s2))
// Prints "true"
print(s3.isStrictSubset(of: s2))
// Prints "false"
超集合

若s2是s1的子集合,則s1是s2的超集合。

let s1: Set = [1, 2, 3, 4]
let s2: Set = [1, 2, 4]
print(s1.isSuperset(of: s2))
// Prints "true"
完全超集合

若s2是s1的完全子集合,則s1是s2的完全超集合。

let s1: Set = [1, 2, 3, 4]
let s2: Set = [1, 2, 4]
let s3: Set = [1, 2, 4]
print(s1.isSuperset(of: s2))
// Prints "true"

print(s2.isSuperset(of: s3))
// Prints "true"
print(s2.isStrictSuperset(of: s3))
// Prints "false"
空集合

若s1中的所有元素在s2中均找不到,且s2中的所有元素在s1中也找不到,代表兩個集合的交集運算結果為空集合。如果為空集合,isDisjoint(_:)傳回true,否則傳回false。

let s1: Set = [1, 2, 3]
let s2: Set = [5, 6]
let s3: Set = [3, 4, 7]
print(s1.isDisjoint(with: s2))
// Prints "true"
print(s1.isDisjoint(with: s3))
// Prints "false"

4.3 字典

字典型態中的每一個元素都包含了key與value兩部分。Key的部分具有唯一性,意思是在字典中找不到兩個一樣的key,value雖然可以重複但資料型態必須一致。字典跟集合一樣,所有元素是無序儲存的。

字典中,key與value用冒號「:」隔開,形成key: value形式。每一組key: value稱為一筆資料,每筆資料間用逗點「,」隔開。Key的用途是用來描述或解釋value,例如「”獅子”: 18」可用來表示動物園的獅子有18隻或18歲。

var zoo = ["獅子": 18, "老虎": 8, "大象": 4, "長頸鹿": 7]

與陣列以及集合一樣,使用let代表常數,之後無法修改內容,使用var代表變數,之後可以再修改內容。存取字典跟存取陣列幾乎一樣,只要把陣列中的索引值改成字典中的key即可,如下:

var zoo = ["獅子": 18, "老虎": 8, "大象": 4, "長頸鹿": 7]
var number = zoo["獅子"]
// number == 18
zoo["大象"] = 5
// zoo == ["獅子": 18, "老虎": 8, "大象": 5, "長頸鹿": 7]

如果字典一開始宣告時沒有初始化字典內容,或是僅初始化為空字典時,必須明確告訴編譯器,該字典的key與value的資料型態。以下這四種方式都是宣告字典變數時同時初始化空字典的語法,四者的結果是一樣的。

var dict1 = [String: Int]()
var dict2 = Dictionary<String, Int>()
var dict3: [String: Int] = [:]
var dict4: Dictionary<String, Int> = [:]

要將資料加入到字典中非常容易,只要key不存於字典中,該字典就會新增這個key,舉例如下:

var zoo: [String: Int] = [:]
zoo["犀牛"] = 5
zoo["彌猴"] = 14
// zoo == ["犀牛": 5, "彌猴": 14]

字典在新增與修改資料上是很方便的,同樣地語句,如果key不存在代表新增一筆資料,如果key已經存在,代表要修改這個key的value。若要刪除一個key,使用removeValue(_:)函數,如下:

zoo.removeValue(forKey: "犀牛")

透過For-In迴圈可以將字典中所有元素經歷一遍,例如:

let zoo = ["獅子": 18, "老虎": 8, "大象": 4, "長頸鹿": 7]
for (animal, number) in zoo {
    print("動物園有\(number)隻\(animal)")
}
// 動物園有18隻獅子
// 動物園有8隻老虎
// 動物園有7隻長頸鹿
// 動物園有4隻大象

~補充說明~
迴圈從字典中取出元素順序並非按照資料輸入順序,只要各元素資料正確即可。

字典可以透過屬性keys可以取得所有的key,屬性values可以取得所有的value,兩者皆以陣列型態傳回。

let zoo = ["獅子": 18, "老虎": 8, "大象": 4, "長頸鹿": 7]
print(zoo.keys)
// Prints ["獅子", "老虎", "長頸鹿", "大象"]
print(zoo.values)
// Prints [18, 8, 7, 4]

雜湊

字典中key的資料型態必須符合Hashable協定才能夠當作key,Swift的基本資料型態(String、Int、Double與Bool…等)預設都已經符合Hashable,因此這些型態都可以拿來當key。此外,列舉型態中的case如果沒有關連值的話也符合Hashable,因此也可以用來當key。列舉與關連值說明請參考第11章。以下這個例子說明了字典中key的資料型態為列舉型態,並且value為closure型態。

enum BulbStatus {
    case on
    case off
}
var bulb: [BulbStatus: ()->Void] = [:]

因此,「.on」與「.off」這兩個key所對應的value分別可以儲存一段程式碼,如下:

bulb[.on] = { print("開燈") }
bulb[.off] = { print("關燈") }

最後我們宣告兩個常數透過「.on」與「.off」這兩個key對應到value的程式碼,並且呼叫他們,如下:

let turnOn = bulb[.on]!
let turnOff = bulb[.off]!

turnOn()
// Prints "關燈"
turnOff()
// Prints "開燈"

下標

字典有一個很特殊的下標語法,目的是當key不存在時,會產生這個key,並且給一個初始值,例如:

var dict: [String: Int] = [:]
dict["n", default: 0] += 1
print(dict["n"]!)
// Prints "1"

這段程式碼與下面的程式碼結果一樣,但語法上簡單一點。

var dict: [String: Int] = [:]
dict["n"] = 0
dict["n"]! += 1
print(dict["n"]!)
// Prints "1"

4.4 向下轉型

Swift允許資料型態可以用更高層級的型態來代替,例如子類別的型態可以用父類別來代替,類別請參考第8章結構與類別。這樣做的目的是讓不同類別的實體可以一致化他們的資料型態。例如報紙(Newspaper)與電視(TV)分別為兩個類別,當我們要將這兩個類別都儲存在陣列中時,我們就需要一個更高層級的類別可以包含Newspaper與TV。物件導向很容易可以做到這樣事情,我們只要定義一個更高層級類別然後讓Newspaper與TV繼承他就好,如下:

class Media {
    
}

class Newspaper: Media {
    var title: String
    init(_ title: String) {
        self.title = title
    }
}

class TV: Media {
    var channel: String
    init(_ channel: String) {
        self.channel = channel
    }
}

現在將陣列型態指定為Media時,這個陣列就可以存放Newspaper與TV這兩種完全不同的資料型態了。

var medias = [
    Newspaper("聯合報"),
    Newspaper("自由時報"),
    TV("華視"),
    Newspaper("蘋果日報"),
    TV("東森新聞")
]

陣列型態會自動推論出Newspaper與TV都有共通的父類別,因此可以省略陣列宣告時的資料型態。現在在陣列中,每個元素的資料型態都被轉型為Media,但元素本身的資料都還在,只是型態暫時轉成更高層級的型態而已。當元素從陣列中取出時,我們必須要恢復他原本的型態,這樣才能使用原本型態中的屬性與方法。這個過程稱為向下轉型(downcasting)。

向下轉型需使用型別轉換運算子「as!」或「as?」。as! 代表已經確定向下轉型的型態是正確的,而 as? 表示不確定是否正確,因此轉換完的型態為Optional型態。使用as?且轉換結果失敗時,會得到nil,所以使用 as? 轉型後必須檢查是否轉換失敗。

let m1 = medias.first as! Newspaper
print(m1.title)
// Prints "聯合報"

if let m2 = medias.last as? TV {
    print(m2.channel)
    // Prints "東森新聞"
}

既然有向下轉型當然也就有向上轉型,陣列medias中的每一個元素全都是經由向上轉型為Media型態,否則陣列中的元素型態無法一致化。通常向上轉型不需要我們自己手動去做,編譯器會自動幫我們處理,如果需要自己手動進行向上轉型的話,使用「as」運算子。因為編譯器會檢查要轉換的型態是否為更高層級,如果不是,編譯器會發出錯誤,因此不需要使用as! 或 as? 運算子。

let paper = Newspaper("經濟日報")
let media = paper as Media

4.5 AnyObject與Any型態

在Swift中,AnyObject與Any相當於是任何資料型態的最高層級型態。AnyObject代表類別型態的最高層級型態,而Any代表所有的型態的最高層級型態,包含類別、結構甚至是函數。換句話說,Any能夠描述的型態範圍比AnyObject來的大。如果我們想要設計一個可以儲存任何型態的陣列或是字典,我們可以這樣設計:

var array: [Any?] = []
var dict: [String: Any?] = [:]

~補充說明~
集合沒有辦法接受Any或AnyObject型態,因為只有符合Hashable協定的資料型態才可以放到集合中,Any與AnyObject都不是Hashable。

加上問號表示連nil都可以放進去。所以,雖然陣列與字典必須儲存同型態的資料,但是如果型態是Any,代表陣列或字典中儲存的資料型態可以非常多元,不限單一型態了。以陣列為例,下面程式碼示範了如何將各種不同型態的資料放入單一陣列中。

var array: [Any?] = []
array.append("汽車")
array.append(10)
array.append(true)
array.append(Double.pi)
array.append([1, 2, 3])

現在所有儲存在array中的資料型態均由原本的型態轉成Any?型態,這代表如果我們將資料從陣列中取出而沒有還原成原本型態時,接下來的運算會出錯,例如要取出陣列第二筆資料(數字10)後做四則運算,我們會得到一個錯誤訊息。

let n = array[1] + 20
// Error: cannot convert value of type 'Any?' to expected argument type 'Int'

將Any或AnyObject轉成另外一個型態跟基本型態(String、Int、Double與Bool…等)之間的轉換不一樣,這裡必須將高層級型態向下轉型為原本的型態,需使用 as! 或 as? 型態轉換運算子,請參考上一節。

let n = array[1] as! Int + 20

我們也可以透過條件判斷來檢查該資料型態是否為我們預期的型態,如下:

if array[1] is Int {
    // 此為整數型態
}

如有需要,我們可以使用For-In迴圈加上switch語句把陣列中每一個元素的資料型態全部列出,如下:

for (index, element) in array.enumerated() {
    switch element {
    case let value as String:
        print("array[\(index)]為字串:\(value)")
    case let value as Int:
        print("array[\(index)]為整數:\(value)")
    case let value as Bool:
        print("array[\(index)]為布林:\(value)")
    case let value as Double:
        print("array[\(index)]為小數:\(value)")
    case is Array<Any>:
        print("array[\(index)]為陣列")
    default:
        print("array[\(index)]為其他型別")
    }
}

// array[0]為字串:汽車
// array[1]為整數:10
// array[2]為布林:true
// array[3]為小數:3.141592653589793
// array[4]為陣列

4.5 共通屬性與函數

陣列、集合與字典在各種不同的程式語言中都是重要且頻繁使用的資料結構,Swift標準函數庫中提供了許多跟陣列、集合與字典操作有關的屬性與函數,可以讓我們在處理一些特定問題上時非常有效率。專屬於陣列、集合或字典的屬性與函數已在前面個別單元說明,因此,本節列出的屬性與函數大部分為三者共通的,只有極少數為特定型態專屬,列在這主要是為了方便對照其他函數。在範例說明上,原則以陣列作為範例,集合與字典就不再贅述,請讀者舉一反三即可,除非三者語法差異甚大且不易理解時才額外增加集合與字典範例。

這裡列出了主要且常用的屬性與函數,其未列出或是之後Swift新版本發佈而本資料尚未來得及更新的,就煩請讀者自行至Apple開發者網站查閱相關說明了。

取得元素數量

名稱:  count
適用:  Array、Set、Dictionary
說明:  取得目前聚集型態中的元素數量,此為唯讀屬性。
範例:

var zoo = ["獅子", "老虎", "大象", "長頸鹿"]
print(zoo.count)
// Prints "4"

修改記憶體配置數量

名稱:  capacity、reserveCapacity(_:)
適用:  Array、Set、Dictionary
說明:  聚集型態的變數每次在新增資料時需要重新配置記憶體空間來儲存新增的資料。如果新增動作太頻繁就會影響程式運作效能,因此可以透過reserveCapacity(_:)函數來預先保留更大的儲存空間以減少記憶體配置次數。reserveCapacity(_:)的參數數值必須超過屬性capacity數值才有意義。屬性capacity為唯讀屬性。
範例:  範例中zoo陣列初始化後的元素數量為4,因此屬性capacity也是4。然後使用reserveCapacity(10)將capacity設定為10,因此之後陣列新增6個以內的元素時都不需要重新配置記憶體,只有新增到第7個元素時才需要重新配置。

var zoo = ["獅子", "老虎", "大象", "長頸鹿"]
print(zoo.capacity)
// Prints "4"

zoo.reserveCapacity(10)
print(zoo.capacity)
// Prints "10"

刪除全部資料並保留記憶體配置

名稱:  removeAll()、removeAll(keepingCapacity:)
適用:  Array、Set、Dictionary
說明:  使用removeAll()函數刪除全部元素後,原本變數或常數佔據的記憶體空間會被作業系統回收,若使用removeAll(keepingCapacity:)並傳入true,元素刪除後會保留記憶體配置不被作業系統回收。
範例:

var numbers = [5, 7, 10, 2, 13]
numbers.removeAll(keepingCapacity: true)
// numbers == []
print(numbers.capacity)
// Prints "5"

判斷是否為空陣列、空集合、空字典

名稱:  isEmpty
適用:  Array、Set、Dictionary
說明:  此為唯讀屬性。若為空陣列、空集合或空字典傳回true否則傳回false。
範例:  以下範例zoo.isEmpty傳回false。

var zoo = ["獅子", "老虎", "大象", "長頸鹿"]
if zoo.isEmpty {
    // 空陣列時程式碼寫這
}

隨機取得元素

名稱:  randomElement()
適用:  Array、Set、Dictionary
說明:  隨機從序列中取回一個元素,若屬性isEmpty為true時傳回nil。
範例:

var zoo = ["獅子", "老虎", "大象", "長頸鹿"]
print(zoo.randomElement()!)

判斷是否存在某個元素

名稱:  contains(_:)
適用:  Array、Set
說明:  若陣列或集合中存在某元素傳回true,否則傳回false。此函數不適用於字典。
範例:

let zoo = ["獅子", "老虎", "大象", "長頸鹿"]
if zoo.contains("大象") {
    print("動物園有大象")
}

判斷是否存在滿足某條件的元素

名稱:  contains(where:_)
適用:  Array、Set、Dictionary
說明:  此函數在陣列與集合的closure傳入一個參數,但字典會傳入兩個參數。
範例:  以下contains(where:)呼叫後全部傳回true。以字典而言,$0代表key,$1代表value。

var array: Array = ["獅子", "老虎", "大象"]
var set: Set = ["獅子", "老虎", "大象"]
var dict: Dictionary = ["獅子": 12, "老虎": 11, "大象": 6]

if array.contains(where: { $0 == "老虎" }) {
    print("陣列中有老虎")
}
if set.contains(where: { $0 == "老虎" }) {
    print("集合中有老虎")
}
if dict.contains(where: { $0 == "老虎" && $1 == 11 }) {
    print("字典中有11隻老虎")
}

判斷所有元素是否都滿足某條件

名稱:  allSatisfy(_:)
適用:  Array、Set、Dictionary
說明:  此函數在陣列與集合的closure傳入一個參數,但字典會傳入兩個參數。
範例:  若型態為字典時,$0代表key,$1代表value。

let grade = [90, 60, 73, 80, 75]
if grade.allSatisfy({ $0 >= 60 }) {
    print("全班成績都達到標準")
}

let zoo = ["獅子": 12, "老虎": 11, "大象": 6, "長頸鹿": 8]
if zoo.contains(where: { $1 > 5 }) {
    print("動物園所有動物數量都超過5隻")
}

取得最大與最小元素

名稱:  max()、min()
適用:  Array、Set
說明:  傳回陣列或集合中的最大值與最小值,如果屬性isEmpty為true時傳回nil。
範例:

let score = [70, 32, 80, 63, 90]
print(score.max()!)
// Prints "90"
print(score.min()!)
// Prints "32"

取得最大與最小元素

名稱:  max(by:)、min(by:)
適用:  Array、Set、Dictionary
說明:  傳回序列中的最大值與最小值,如果屬性isEmpty為true時傳回nil。此函數可傳入closure,相當於進階版的max()與min(),因此可適用於字典。
範例:

let zoo = ["獅子": 12, "老虎": 11, "大象": 6, "長頸鹿": 8]
let maxAnimal = zoo.max(by: {$0.value < $1.value})!
let minAnimal = zoo.max(by: {$0.value > $1.value})!

print("\(maxAnimal.key)數量最多,有\(maxAnimal.value)隻")
print("\(minAnimal.key)數量最少,有\(minAnimal.value)隻")
// Prints "獅子數量最多,有12隻"
// Prints "大象數量最少,有6隻"

過濾

名稱:  filter(_:)
適用:  Array、Set、Dictionary
說明:  傳回序列中所有符合條件的元素。
範例:  

let numbers = [7, 6, 2, 8, 9, 1]
let filter = numbers.filter { $0 > 5 }
// filter == [7, 6, 8, 9]

對映

名稱:  map(_:)
適用:  Array、Set、Dictionary
說明:  將原有序列中的元素根據對映函數對映到另一個領域。運算結果會產生新序列,原有序列內容不變。
範例:  此範例將數字1, 2, 3, 4經由平方函數對映成1, 4, 9, 16

var array: Array = [1, 2, 3, 4]
var set: Set = [1, 2, 3, 4]
var dict: Dictionary = ["A": 1, "B": 2, "C": 3, "D": 4]

let mappedArray = array.map { $0 * $0 }
let mappedSet = set.map { $0 * $0 }
let mappedDict = dict.map { $0.value * $0.value }

扁平對映

名稱:  flapMap(_:)
適用:  Array、Set、Dictionary
說明:  此函數常用於陣列。將二維陣列扁平化成一維陣列。運算結果會產生新陣列,原有陣列內容不變。
範例:

let array = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
let flatMapped = array.flatMap { $0 }
// flatMapped == [1, 2, 3, 4, 5, 6, 7, 8, 9]

範例:若為三維陣列,連續使用兩次flapMap(_:)就可以轉成一維陣列。

let array = [[[1, 2, 3], [4, 5, 6], [7, 8, 9]]]
let flatMapped = array.flatMap { $0 }.flatMap { $0 }
//flatMapped == [1, 2, 3, 4, 5, 6, 7, 8, 9]

結實對映

名稱:  compactMap(_:)
適用:  Array、Set
說明:  移除nil元素。運算結果產生新序列,原有序列內容不變。字典型態需使compactMapValue(_:),請參考下一個函數。
範例:

let array = ["獅子", "老虎", nil, "長頸鹿", nil]
let set: Set = ["獅子", "老虎", nil, "長頸鹿"]

let compactArray = array.compactMap { $0 }
let compactSet = set.compactMap { $0 }
// compactArray == ["獅子", "老虎", "長頸鹿"]
// compactSet ==  ["獅子", "老虎", "長頸鹿"]

結實數值對映

名稱:  compactMapValues(_:)
適用:  Dictionary
說明:  移除字典中value為nil的key。運算結果產生新字典,原有字典內容不變。
範例:

let zoo = ["獅子": 12, "老虎": nil, "大象": nil, "長頸鹿": 8]
let compactZoo = zoo.compactMapValues { $0 }
// compactZoo == ["獅子": 12, "長頸鹿": 8]

將全部元素對映成一個元素

名稱:  reduce(_:_:)
適用: Array、Set、Dictionary
說明:  將聚集型態中全部的元素經由運算後對映成一個元素,運算結果產生新序列,原有序列資料不變。函數reduce(_:_:)有兩個參數,由原始定義來看,第二個參數為closure,形式為 (Result, Element) -> Result。Element是從序列中取出的元素,跟Result進行運算後將結果再放回Result中,有點類似「result = result + element」這樣的運算式。要進行這樣的運算,Result必須要有初始值,reduce(_:_:)的第一個參數就是Result的初始值。
範例:  計算1 + 2 + 3 + … + 10。

let numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
let sum = numbers.reduce(0) { $0 + $1 }
print(sum)
// Prints "55"

範例: 同樣計算1 + 2 + 3 + … + 10。

let numbers = Array(1...10)
let sum = numbers.reduce(0, +)
print(sum)
// Prints "55"

範例:  計算動物園中的動物數量。

let zoo = ["獅子": 12, "老虎": 11, "大象": 6, "長頸鹿": 8]
let total = zoo.reduce(0) { $0 + $1.value }
print(total)
// Prints "37"

將全部元素對映成一個元素

名稱: reduce(into:_:)
適用:  Array、Set、Dictionary
說明:  函數reduce(into:_:)與reduce(_:_:)非常類似,也是將序列中所有元素對映成一個元素。但此函數中的closure形式為 (inout Result, Element) -> Void,由inout可知運算結果會直接修改參數Result而不是將結果用return方式傳回。參數加上inout修飾子請參考第6章函數。
範例:  計算字串中每個字元出現的次數。

let str = "aaaabbcccccddd"
let result = str.reduce(into: [:]) { (count, ch) in
    count[ch, default: 0] += 1
}
print(result)
// Prints ["a": 4, "b": 2, "c": 5, "d": 3]

自帶迴圈forEach

名稱:  forEach(_:)
適用:  Array、Set、Dictionary
說明:  依序取得聚集型態中每一個元素並傳至closure中,此函數與使用For-In迴圈效果一樣。
範例:  以下以陣列為例,若為字典,$0表示key,$1表示value。

let zoo = ["獅子", "老虎", "大象", "長頸鹿"]
zoo.forEach {
    print("動物園有\($0)")
}
// 動物園有獅子
// 動物園有老虎
// 動物園有大象
// 動物園有長頸鹿

列舉化

名稱:  enumerated()
適用:  Array、Set、Dictionary
說明:  將原本聚集型態中的每一個元素產生 (n, x) 的tuple型態。其中n代表該元素索引值,x代表該元素。然後傳回 (n, x) 所形成的序列結構,透過Array可以轉成陣列型態,原本序列內容不變。
範例:

let zoo = ["獅子", "老虎", "大象", "長頸鹿"]
print(Array(zoo.enumerated()))
// [(offset: 0, element: "獅子"),
//  (offset: 1, element: "老虎"),
//  (offset: 2, element: "大象"),
//  (offset: 3, element: "長頸鹿")]

範例:

let zoo = ["獅子", "老虎", "大象", "長頸鹿"]
for (index, animal) in zoo.enumerated() {
    print("\(index): \(animal)")
}
// 0: 獅子
// 1: 老虎
// 2: 大象
// 3: 長頸鹿

發表迴響