了解 swift 数组如何在底层工作,以及如何优化它们——使用 ContiguousArray、它们的比较等。
- _ContiguousArrayStorage - 为存储元素分配内存,通过索引提供快速访问。
- _ArrayBridgeStorage - 一种允许您同时使用本机存储和 NSArray 的抽象。
- _ArrayBuffer<T> - 写入时复制的实现。
- Array<T> - 公共接口。
增加数组的大小
每个数组保留特定数量的内存来存储其内容。 当您将项目添加到数组并且该数组开始超过其保留容量时,该数组会分配大量内存并将其项目复制到新的保管库,其中新保管库的大小与旧保管库的许多大小相同。
这种指数增长策略意味着追加一个元素在恒定时间内发生,平均许多追加操作的性能。 触发重新分配的追加操作有性能成本,但随着阵列变大,它们发生的频率越来越低。
示例:创建一个包含 10 个元素的数组。
Swift 会为这个数组分配足够的容量来存储这 10 个元素,所以 array.capacity 和 array.count 都等于 10。
var array = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
print(array.capacity) // 10
print(array.count) // 10
让我们添加 11 和 12 个元素。 数组没有这个容量,所以它需要释放空间——它会找到内存来存储更多元素,将数组复制到那里,然后添加 11 和 12 个元素。 这些插入的执行时间很复杂——O(n),其中 n 是数组中元素的数量。
array.append(11)
array.append(12)
print(array.capacity) // 20
print(array.count) // 12
因此,当您将 11 和 12 个元素添加到容量为 10 的数组时,swift 会创建一个大小为 20 的数组。当我们超过这个大小时,下一个容量将是 40,然后是 80,汗水 160,等等 上。
如果您知道大约要保存多少项目,请使用 reserveCapacity(_:)。 这将允许您避免中间重新分配。
使用容量和计数属性来确定一个数组可以存储多少元素而不分配大量资源。
ar stringArray = Array<String>()
stringArray.reserveCapacity(128)
如果您的数组的 Element 类型是类或 @objc 协议,并且您不需要将数组桥接到 NSArray 或将数组传递给 Objective-C API,则使用 ContiguousArray 可能比 Array 更高效且性能更可预测。 如果数组的 Element 类型是结构体或枚举,Array 和 ContiguousArray 的效率应该差不多。
ContiguousArray 类型是一个专门的数组,它始终将其元素存储在一个连续的内存区域中。 这与 Array 形成对比,后者可以将其元素存储在内存的连续区域或 NSArrayinstance 中,如果其元素类型是类或 @objc 协议。
数组将连续存储:
- 在 Swift 中创建的数组总是连续存储的;
- 结构和枚举的数组将始终连续存储;
- 没有 Objective-C 运行时的平台(即非达尔文平台)上的数组总是连续存储的;
- 数组不会被连续存储的唯一情况是它是否属于类并且已从 NSArray 桥接
- 即使那样,在许多情况下,NSArray 将被连续存储,并且可以免费或摊销成本提供指针
Array 和 ContiguousArray 的比较
import Foundation
protocol Possibles {
init(repeating: Bool, count: Int)
subscript(index: Int) -> Bool { get set }
var count: Int { get }
}
extension Array: Possibles where Element == Bool {}
extension ContiguousArray: Possibles where Element == Bool {}
func lazySieveOfEratosthenes<Storage: Possibles>(makeStorage: () -> Storage) -> [Int] {
var possibles = makeStorage()
let limit = possibles.count - 1
return (2 ... limit).lazy.filter { i in // Lazy, so that `possibles` is updated before it is used.
possibles[i]
}.map { i in
stride(from: i * i, through: limit, by: i).lazy.forEach { j in
possibles[j] = false
}
return i
}.reduce(into: [Int]()) { (result, next) in
result.append(next)
}
}
func forSieveOfEratosthenes<Storage: Possibles>(makeStorage: () -> Storage) -> [Int] {
var possibles = makeStorage()
let limit = possibles.count - 1
var result = [Int]()
for i in 2 ... limit where possibles[i] {
var j = i * i
while j <= limit {
possibles[j] = false
j = i
}
result.append(i)
}
return result
}
func test(_ name: String, _ storageType: String, _ function: (Int) -> [Int]) {
let start = Date()
let result = function(100_000)
let end = Date()
print("\(name) \(storageType) biggest: \(result.last ?? 0) time: \(end.timeIntervalSince(start))")
}
test("for ", "Array ") { limit in
forSieveOfEratosthenes {
Array(repeating: true, count: limit 1)
}
}
test("for ", "ContiguousArray") { limit in
forSieveOfEratosthenes {
ContiguousArray(repeating: true, count: limit 1)
}
}
test("lazy ", "Array ") { limit in
lazySieveOfEratosthenes {
Array(repeating: true, count: limit 1)
}
}
test("lazy ", "ContiguousArray") { limit in
lazySieveOfEratosthenes {
ContiguousArray(repeating: true, count: limit 1)
}
}
结果:
for Array biggest: 99991 time: 41.016937017440796
for ContiguousArray biggest: 99991 time: 40.648478984832764
lazy Array biggest: 99991 time: 3.3549970388412476
lazy ContiguousArray biggest: 99991 time: 3.5851539373397827
另一个:
for Array biggest: 99991 time: 41.801795959472656
for ContiguousArray biggest: 99991 time: 42.37710893154144
lazy Array biggest: 99991 time: 3.438219904899597
lazy ContiguousArray biggest: 99991 time: 3.4085270166397095
启动是在具有以下配置的笔记本电脑上完成的:
- macOS 蒙特雷(12.3 版)
- 处理器 2.3 GHz 8 核 Intel core i9
- 内存 16 GB DDR4
- Xcode 版本 13.3.1 (13E500a),游乐场
代码示例取自here,我建议您查看整个线程。 如您所见,Array 并不总是比 ContiguousArray 慢。
带类的数组
如果 Array 中的元素是类的实例,则语义是相同的,尽管它们起初看起来可能不同。 在这种情况下,存储在数组中的值是对数组外对象的引用。 当您更改单个数组中的对象引用时,只有该数组具有对新对象的引用。 然而,如果两个数组包含对同一个对象的引用,您可以从两个数组观察到该对象属性的变化。
class MyClass {
var name = "Name"
}
var firstArray = [MyClass(), MyClass()]
var secondArray = firstArray
firstArray[0].name = "Another Name"
print(firstArray[0].name) // "Another name"
print(secondArray[0].name) // "Another name
- 替换、添加和删除仅适用于操作适用的数组:
firstArray[0] = MyClass()
print(firstArray[0].name) // "Name"
print(secondArray[0].name) // "Another name
Big-O 算法复杂性与操作:
- O (1) — 恒定时间(最快)。 通过索引访问数组中的元素:
let array = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
array[3]
- O(n log n) — 对数时间(比常数时间稍差)。 升序排序,标准函数:
var people = ["Sandra", "Mike", "James", "Donald"]
people.sort()
- O(n) - 线性时间(比对数复杂度稍差)。 使用标准函数 forEach 计算元素的总和:
let numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
var sumOfNumbers = 0
numbers.forEach {
sumOfNumbers = $0
}
print(sumOfNumbers) // 55
- O(n ²) — 二次时间(略差于 O (n log n)。遍历 2D 数组:
var twoDimensionalArray = [["first one", "second one"], ["first two", "second two"], ["first third", "second third"]]
for i in 0..<twoDimensionalArray.count {
for n in 0..<twoDimensionalArray[i].count {
print(twoDimensionalArray[i][n])
}
}
结论
Swift 中的数组是一种很好的数据类型,用于组织有序集合,您可以在其中存储各种元素,但请记住值和引用类型的语义。 您可以执行各种操作(添加、删除等),不仅可以使用 Array,还可以使用 ContiguousArray、NSArray、ArraySliceand 等。
谢谢阅读。
,