Swift-技巧(七)重識 Array

摘要

iOS 開發,尤其是從 OC 轉換到 Swift,對 Array 需要重新瞭解,Swift 中保留了什麼屬性,增加了什麼屬性,內存存儲是什麼情況等等,瞭解了這些,在使用 Array 的時候可以更符合 Swift 的思想,也方便了自己。

Array 是應用程序中最常用的數據類型之一,可以使用 Array 來處理應用程序中的數據。使用 Array 類型來保存單一類型的元素。數組可以存儲任何類型的元素—— 從 Int 到 String,甚至是 Class,但是數組中元素必須是同一類型

Swift 中可以使用數組字面量創建數組,若沒有定義數組中元素類型,Swift 會自動推斷數組中元素的類型。例如:

// 數組元素類型爲 Int
let numbers = [1, 2, 3]

// 數組元素類型爲 String
let names = ["li", "zhang"]

也可以通過在聲明中指定數組的元素類型來創建一個空數組,例如:

// 元素類型爲 Int 的空數組
var emptyInt: [Int] = []

// 元素烈性爲 Double 的空數組
var emptyDouble: Array<Double> = Array()

如果需要固定數量的默認值來初始化數組,可以使用 Array(repeating:count:) 創建:

var digitCounts = Array(repeating: 1, count: 3)
print(digitCounts)
// Prints "[1, 1, 1]"

訪問數組元素

當需要對數組所有元素執行操作時,可以使用 for-in 來循環遍歷數組的內容。

for name in names {
	print(name)
}
// Print "li"
// Print "zhang"

這裏可以使用 isEmpty 屬性快速檢查數組中是否包含任何元素,等同於使用 count 屬性查找數組中的元素數量,並判斷是否爲 0

print(numbers.isEmpty) // Print false
print(numbers.count == 0) // Print false

使用 firstlast 屬性可以安全地訪問數組的第一個元素和最後一個元素,如果數組爲空,就返回 nil

print(number.first) // Print "1"
print(number.last) // Print "3"

print(emptyInt.first, emptyInt.last, separator: ", ")
// Prints "nil, nil"

可以通過下標訪問數組中的單個元素,非空數組的第一個元素下標爲 0,數組的下標範圍從 0 到數組的 count(不包括 count)。使用負數、等於或者大於 count 的索引,會引發運行時錯誤,比如:

print(number[0], number[2], separator: ", ")
// Prints "1, 3"

print(emptyInt[0])
// Triggers runtime error: Index out of range

添加和刪除元素

假如你需要存儲一個入職員工的名字列表。在這期間,你需要添加和刪除名字。

var staffs = ["zhang", "wang", "li"]

使用 append(_:) 方法將單個元素添加到數組的尾部。使用 append(contentsOf:) 可以同時添加多個元素,參數可以是另外一個數組或者元素類型相同的序列

staffs.append("zhao")
staffs.append(contentsOf: ["song", "suo"])
// ["zhang", "wang", "li", "zhao", "song", "suo"]

你也可以在數組中間添加新的元素,使用 insert(_:at:) 函數插入單個元素,使用 insert(contentsOf:at:) 方法插入另一個集合或者數組字面量的多個元素。索引處和索引後面的元素都往後移,騰出空間。

staffs.insert("ding", at: 3)
// ["zhang", "wang", "li", "ding", "zhao", "song", "suo"] 

要從數組中刪除元素,可以使用 remove(at:)removeSubrange(_:)removeLast() 函數。

staffs.remove(at: 0)
// ["wang", "li", "ding", "zhao", "song", "suo"] 

staffs.removeLast()
// ["wang", "li", "ding", "zhao", "song"] 

通過將新值賦值給下標,達到用新值替換現有的元素的效果

if let i = staffs.firstIndex(of: "ding") {
	staffs[i] = "bian"
}
// ["wang", "li", "bian", "zhao", "song"]  

增加數組的大小(重點)

每個數組都會保留特定數量的內存來保存其內容。當向數組中添加元素時,該數組超過其預留的容量,該數組就會分配更大的內存空間,並將它的所有元素賦值到新的存儲空間。添加一個元素的時間是固定的,相對來說性能是平均的。但是重新分配的操作會帶來性能成本,隨着數組的增大,這些操作發生的頻率也會越來越低。

如果知道大約需要存儲多少元素,在添加到元素之前使用 reserveCapacity(_:) 函數,避免中間重新分配。使用 countcapacity 屬性來確定數組在不分配更大存儲空間的情況下還可以存儲多少元素。

var array = [1, 2]
array.reserveCapacity(20)

對於大多數 Element 類型的數組,存儲區域是一個連續的內存,對於元素類型是 class 或者 objc protocol 類型的數組,該存儲可以是一個連續的內存,或者 NSArray 的實例。因爲 NSArray 的任何子類都可以是一個 Array,所以這種情況下不能保證是內存塊或者 NSArray 的實例

修改數組副本(重點)

每個數組都有一個獨立的存儲空間,存儲着數組中包含的所有元素的值。對於簡單類型,如整數或者其他結構,當更改一個數組中的值時,該元素的值不會在數組的任何副本中更改。例如:

var numbers = [4, 5, 6]
var numbersCopy = numbers

numbers[0] = 9
print(numbers)
// Prints "[9, 5, 6]"
print(numbersCopy)
// Prints "[4, 5, 6]"

如果數組的元素是類的實例。在本例中,存儲在數組中的值是對存在於數組之外的對象引用。如果更改一個數組中對象的引用 ,則只有該數組有對新對象的引用。但是,如果兩個數組包含對同一個對象的引用,則可以從兩個數組中看到該對象屬性的更改,例如:

class InterR {
	var value = 10
}

var integers1 = [InterR(), InterR()]
var integers2 = integers1

integers1[0].value = 100
print(integers2[0].value)
// Prints "100"

integers1[0] = InterR()
print(integers1[0].value)
// Prints "10"
print(integers2[0].value)
// Prints "100"

和標準庫中的所有可變大小集合一樣,數組也使用 copy-on-write 優化。多個副本共享同一個存儲空間,直到修改其中一個副本爲止。當發生這種情況時,被修改的數組會創建新的存儲空間存儲,然後在對應的位置修改。copy-on-write 優化可以減少複製的數量。

這就說明,如果一個數組與其他副本共享存儲空間,對該數組的第一次更改操作會導致複製該數組的成本。之後就可以作爲唯一的擁有者來對它進行操作。

下面例子中,將創建一個數組和兩個共享相同存儲的副本。當原始數組被修改時,它會在修改前對其存儲進行唯一複製。然後進行修改。而兩個副本繼續共享原來的存儲空間。

var numbers = [7, 8, 9]
var copy1 = numbers
var copy2 = numbers

numbers[0] = 100
numbers[1] = 200
numbers[2] = 300

// numbers: [100, 200, 300]
// copy1 和 copy 2 : [7, 8, 9]

Array 和 NSArray 之間轉換(重點)

當需要訪問 NSArray 實例的 API 時,使用類型轉換操作符 as 來轉換 Array 實例。爲了保證轉換成功,數組的 Element 類型必須是一個 class 類型,一個 @objc protocol 或者一個連接到 Foundation 類型。

下面例子展示瞭如何使用 write(to:atomically:) 函數將一個 Array 實例連接到 NSArray。在這個例子中,colors 數組可以轉換到 NSArray,因爲 colors 數組的 String 類型元素轉換到 NSString。另一個,編譯器阻止轉換 moreColors 數組,因爲它的 Element 類型是 Optional, 它不能轉換 Foundation 類型。

let colors = ["periwinkle", "rose", "moss"]
let moreColors: [String?] = ["ochre", "pine"]

let url = URL(fileURLWithPath: "names.plist")
(colors as NSArray).write(to: url, atomically: true)
// true

(moreColors as NSArray).write(to: url, atomically: true)
// error: cannot convert value of type '[String?]' to type 'NSArray'

如果數組的元素已經是 class @objc protocol 的實例,那麼從 Array 到 NSArray 的轉換隻需要 O(1) 時間和空間,否則,它需要 O(n) 個時間和空間

當目標數組的元素類型是一個 class@objc protocol 時,從 NSArray 到 Array 的轉換首先調用數組上的 copy(with:) 函數來得到一個不可變的副本,然後執行額外的 Swift bookkeeping work,需要 O(1) 時間。對於已經不可變的 NSArray 實例,copy(with:) 通常在 O(1) 時間內返回相同的數組,否則,複製性能將不確定。如果 copy(with:) 返回相同的數組,NSArray 和 Array 的實例使用相同的 copy-on-write 優化共享存儲空間,當兩個 array 實例共享存儲空間時,使用相同的優化。

當目標數組的元素類型是轉換 Foundation 類型的非 class 類型時,從 NSArray 到 Array 的轉換會在 O(n) 時間內把元素轉換複製到連續的存儲空間。比如,從 NSArray 轉換到 Array 執行這樣的複製。當訪問 Array 實例的元素時,不需要進一步的轉換。

注意

ContiguousArrayArraySlice 類型沒有轉換,這些類型的實例總是有一個連續的內存空間作爲它的的存儲。

題外話

時間倉促,說的東西可能不全面,在你查看的過程中遇到什麼問題,評論區給我留言,我會盡快回復

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章