现在位置: 首页 > Swift 教程 > 正文

Swift 数组

数组(Array)用来按顺序存放同一类型的多个元素,是 Swift 中最常用的集合类型。

它保留了元素的插入顺序,可以按下标随机访问,也能动态地追加和删除元素。

本篇讲数组的创建、下标访问、增删改、三种遍历写法,以及 Swift 数组最重要的特性之一:值语义与写时复制。

键值对集合请见《Swift 字典》,无序集合与高阶函数请见《Set 与高阶函数》。


创建数组

数组的类型写作方括号里放元素类型,比如 [String][Int],字面量则用方括号直接列出元素。

类型和字面量的括号长得很像,但一个是类型标注,一个是值。

实例

import Foundation

// 数组字面量,元素类型由编译器推断为 [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 开始,范围是 0count - 1

下标越界会直接触发运行时陷阱,程序终止,不会像可选值那样返回 nil

想安全取值,用 firstlast,它们返回的是可选值。

实例

import Foundation

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:) 等方法。

实例

import Foundation

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:),可以一次追加整个序列。

注意 insertremove 会移动后续元素,在大数组中间频繁操作时开销较高。


遍历数组

for-in 可以逐个取出元素,这是最常用的写法。

需要下标时,配合 enumerated() 拿到「下标 + 元素」的元组,或者直接遍历 indices

实例

import Foundation

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 生成一个新数组。


值语义与写时复制

把数组赋给另一个变量,或者作为参数传给函数,得到的是独立的副本。

Swift 数组的写时复制:赋值时只复制缓冲区引用、引用计数加一,首次写入且引用计数大于 1 时才真正复制出私有副本

修改其中一个,另一个不受影响。

实例

import Foundation

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) 判断,或改用 firstlast 这类返回可选值的属性。

repeating 和 count 参数顺序记不清怎么办

记住「先说什么值、再说几个」,也就是 [Int](repeating: 0, count: 3)

写反成 [Int](count: 3, repeating: 0) 会直接编译报错,编译器会提示没有匹配的初始化器。

怎么快速找出某个元素的下标

firstIndex(of:),它返回 Int?,元素不存在时是 nil

只想判断存不存在,用 contains(_:) 更直接。