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 类型没有转换,这些类型的实例总是有一个连续的内存空间作为它的的存储。

题外话

时间仓促,说的东西可能不全面,在你查看的过程中遇到什么问题,评论区给我留言,我会尽快回复

原文地址:https://www.cnblogs.com/shsuper/p/15594944.html