Swift 数组
数组(Array)用来按顺序存放同一类型的多个元素,是 Swift 中最常用的集合类型。
它保留了元素的插入顺序,可以按下标随机访问,也能动态地追加和删除元素。
本篇讲数组的创建、下标访问、增删改、三种遍历写法,以及 Swift 数组最重要的特性之一:值语义与写时复制。
键值对集合请见《Swift 字典》,无序集合与高阶函数请见《Set 与高阶函数》。
创建数组
数组的类型写作方括号里放元素类型,比如 [String]、[Int],字面量则用方括号直接列出元素。
类型和字面量的括号长得很像,但一个是类型标注,一个是值。
实例
// 数组字面量,元素类型由编译器推断为 [String] 与 [Int]
var sites: [String] = ["Runoob", "RUNOOB"]
let numbers = [1, 2, 3]
// 空数组,两种写法等价
var empty: [Int] = []
var empty2 = [Int]()
// 创建 3 个 0 组成的数组
let zeros = [Int](repeating: 0, count: 3)
// 用区间创建数组
let rangeArray = Array(1...5)
print(sites, numbers, zeros, rangeArray, separator: " | ")
print(empty.count, empty2.isEmpty)
以上代码执行输出结果为:
["Runoob", "RUNOOB"] | [1, 2, 3] | [0, 0, 0] | [1, 2, 3, 4, 5] 0 true
几种创建方式对照如下。
| 写法 | 说明 |
|---|---|
| [1, 2, 3] | 字面量,元素类型由编译器推断 |
| var empty: [Int] = [] | 空数组,同时给出类型标注 |
| [Int]() | 调用类型的初始化器,得到空数组 |
| [Int](repeating: 0, count: 3) | 由重复值和数量创建 |
| Array(1...5) | 由区间或另一个序列创建 |
注意:重复值初始化器的参数顺序容易记反,是 repeating 在前、count 在后。老教程里的 [Int](count:repeatedValue:) 在 Swift 3 就被替换,现在已经无法编译。
访问元素与下标
用下标访问元素,下标从 0 开始,范围是 0 到 count - 1。
下标越界会直接触发运行时陷阱,程序终止,不会像可选值那样返回 nil。
想安全取值,用 first 和 last,它们返回的是可选值。
实例
let sites = ["Runoob", "RUNOOB", "www.runoob.com"]
print(sites[0])
print(sites.first ?? "空")
print(sites.last ?? "空")
print(sites.count, sites.isEmpty)
print(sites.firstIndex(of: "RUNOOB") ?? -1)
print(sites.contains("Runoob"))
print(sites.indices.contains(3))
以上代码执行输出结果为:
Runoob Runoob www.runoob.com 3 false 1 true false
indices 是数组的有效下标范围,先用 contains(_:) 判断再取值,可以避免越界崩溃。
上例中数组只有 3 个元素,合法下标是 0、1、2,所以 indices.contains(3) 为 false。
增删改
要修改数组,必须先用 var 声明,let 声明的数组内容不可变。
追加用 append,插入用 insert(_:at:),删除用 remove(at:) 等方法。
实例
var sites = ["Runoob", "RUNOOB"]
sites.append("www.runoob.com") // 追加到末尾
sites.insert("Swift", at: 0) // 插入到指定位置
sites[1] = "RUNOOB 教程" // 修改指定位置
print(sites)
let removed = sites.remove(at: 0) // 删除指定位置,返回被删除的元素
print("删除:\(removed)")
sites.removeLast() // 删除最后一个
print(sites)
以上代码执行输出结果为:
["Swift", "RUNOOB 教程", "RUNOOB", "www.runoob.com"] 删除:Swift ["RUNOOB 教程", "RUNOOB"]
常用的增删改方法整理如下。
| 方法 | 作用 | 返回值 |
|---|---|---|
| append(_:) | 在末尾追加一个元素 | 无 |
| insert(_:at:) | 在指定下标插入元素 | 无 |
| remove(at:) | 删除指定下标的元素 | 被删除的元素 |
| removeLast() | 删除最后一个元素 | 被删除的元素 |
| removeAll() | 清空数组 | 无 |
append 还有一个批量版本 append(contentsOf:),可以一次追加整个序列。
注意 insert 与 remove 会移动后续元素,在大数组中间频繁操作时开销较高。
遍历数组
用 for-in 可以逐个取出元素,这是最常用的写法。
需要下标时,配合 enumerated() 拿到「下标 + 元素」的元组,或者直接遍历 indices。
实例
let sites = ["Runoob", "RUNOOB", "www.runoob.com"]
// 只要元素
for site in sites {
print(site)
}
// 需要下标:enumerated() 返回 (offset, element)
for (index, site) in sites.enumerated() {
print("\(index): \(site)")
}
// 只要下标,再用下标取值
for i in sites.indices {
print("\(i) -> \(sites[i])")
}
以上代码执行输出结果为:
Runoob RUNOOB www.runoob.com 0: Runoob 1: RUNOOB 2: www.runoob.com 0 -> Runoob 1 -> RUNOOB 2 -> www.runoob.com
注意:enumerate() 是 Swift 2 的旧写法,Swift 3 起改名为 enumerated()。另外,遍历期间不要增删元素,这会让遍历行为变得不可预期,需要过滤或删除时先用 filter 生成一个新数组。
值语义与写时复制
把数组赋给另一个变量,或者作为参数传给函数,得到的是独立的副本。
修改其中一个,另一个不受影响。
实例
var a = [1, 2, 3]
var b = a // 此时 a 与 b 共享存储,尚未真正复制
b.append(4) // 写入 b 时才发生复制,a 不受影响
print("a = \(a), b = \(b)")
以上代码执行输出结果为:
a = [1, 2, 3], b = [1, 2, 3, 4]
Swift 并不会在赋值时立刻复制整份数据,而是先共享同一块存储,只在引用计数上加一。
当其中一方要写入,且发现这块存储还被别人引用时,才真正复制出独立的一份,这套机制叫写时复制(Copy-On-Write,COW)。
它既保证了值语义,又避免了大数组在传参时被反复拷贝,性能与语义两头都不吃亏。
原理详见《Swift 值语义与写时复制》。
注意:结构体里如果嵌套了引用类型,值语义会被打破。修改副本中的引用对象,原对象也会看到变化,这类情况需要自己实现深拷贝。
常见问题
下面是数组使用中最常见的几个问题。
数组是值类型还是引用类型
是值类型。赋值时语义上等同于复制,实际由写时复制优化,只有真正写入才产生副本。
这一点与 Objective-C 中共享同一个对象的 NSArray 完全不同。
下标越界会怎样
会触发运行时陷阱,程序直接终止,不会静默返回 nil,也没有「越界返回默认值」的行为。
不确定下标是否合法时,先用 indices.contains(index) 判断,或改用 first、last 这类返回可选值的属性。
repeating 和 count 参数顺序记不清怎么办
记住「先说什么值、再说几个」,也就是 [Int](repeating: 0, count: 3)。
写反成 [Int](count: 3, repeating: 0) 会直接编译报错,编译器会提示没有匹配的初始化器。
怎么快速找出某个元素的下标
用 firstIndex(of:),它返回 Int?,元素不存在时是 nil。
只想判断存不存在,用 contains(_:) 更直接。
