Swift 字典
字典(Dictionary)用来按键值对存放元素,通过键快速找到对应的值,查找过程与元素数量无关。
它和数组一样是值类型,赋值和传参时保持独立;但它不保证顺序,这一点和数组正好相反。
本篇讲字典的创建、增删改、default 下标、遍历、mapValues 与 merging,以及键的 Hashable 要求。
按位置存放元素请见《Swift 数组》。
创建字典
字典的类型写作方括号里放「键类型: 值类型」,比如 [String: Int]。
字面量用 键: 值 的形式列出,空字典有两种写法。
实例
// 字典字面量的类型标注是 [键类型: 值类型]
var ages: [String: Int] = ["Runoob": 10, "RUNOOB": 5]
// 空字典,两种写法等价
let empty = [String: Int]()
var empty2: [String: Int] = [:]
print(ages.count, empty.isEmpty, empty2.count)
以上代码执行输出结果为:
2 true 0
几种创建方式对照如下。
| 写法 | 说明 |
|---|---|
| ["Runoob": 10, "RUNOOB": 5] | 字面量,键值类型由编译器推断 |
| var ages: [String: Int] = [:] | 空字典,用空字面量加类型标注 |
| [String: Int]() | 调用类型的初始化器,得到空字典 |
| Dictionary(uniqueKeysWithValues:) | 由键值对序列创建,键必须唯一 |
注意:单独写 let empty = [:] 无法编译,编译器推不出键和值的类型,必须补上类型标注。
增删改
用下标赋值即可新增或修改:键不存在就是新增,键已存在就是覆盖。
删除用 removeValue(forKey:),它会把被删除的值作为可选值返回。
实例
var ages: [String: Int] = ["Runoob": 10, "RUNOOB": 5]
ages["Swift"] = 11 // 键不存在,新增
ages["Runoob"] = 12 // 键已存在,覆盖
print(ages.count)
// removeValue 删除并返回被删除的值;键不存在时返回 nil
let removed = ages.removeValue(forKey: "RUNOOB")
print(removed ?? -1, ages.count)
// updateValue 返回被替换掉的旧值;键不存在时返回 nil
if let old = ages.updateValue(20, forKey: "Runoob") {
print("旧值 \(old)")
}
print(ages["Runoob"] ?? 0)
以上代码执行输出结果为:
3 5 2 旧值 12 20
三个写入方法的区别整理如下。
| 方法 | 作用 | 返回值 |
|---|---|---|
| dict[key] = value | 新增或覆盖 | 赋值语句没有返回值 |
| updateValue(_:forKey:) | 新增或覆盖 | 被替换的旧值,键不存在时是 nil |
| removeValue(forKey:) | 按键删除 | 被删除的值,键不存在时是 nil |
| removeAll() | 清空字典 | 无 |
下标赋值返回 Void,拿不到旧值;需要知道「原来是什么」时就用 updateValue。
注意:删除的方法名是 removeValue(forKey:)。老教程里的 removeValueForKey 早已改名,现在无法编译。
default 下标
普通下标返回的是可选值,取值前要解包,做计数、累加这类操作时会很啰嗦。
带 default 的下标在键不存在时直接给出默认值,并且允许就地修改。
实例
var counts = ["Runoob": 1]
// 键不存在时先给出默认值 0,再做 += 1
counts["RUNOOB", default: 0] += 1
counts["RUNOOB", default: 0] += 1
print(counts["RUNOOB"] ?? 0)
print(counts.count)
以上代码执行输出结果为:
2 2
第一次执行时 RUNOOB 还不存在,下标表达式先返回默认值 0,加一后写回字典,键才真正被创建。
第二次执行时键已存在,取出 1 加一得到 2。
注意:不带 default 的下标返回 Int?,直接写 counts["RUNOOB"] += 1 会编译不过,因为不能对可选值做加法。
遍历字典
字典是无序集合,遍历顺序不保证,也不等于插入顺序,更不能跨版本、跨平台地假设它稳定。
需要稳定输出时,先对键排序再遍历。
实例
let ages = ["Runoob": 10, "RUNOOB": 5, "Swift": 11]
// 直接遍历能取到全部键值对,但顺序不保证,这里只统计总和
var total = 0
for (_, value) in ages {
total += value
}
print(total)
// 需要稳定顺序时先按 key 排序
for (key, value) in ages.sorted(by: { $0.key < $1.key }) {
print("\(key): \(value)")
}
print(ages.keys.sorted())
print(ages.values.sorted())
以上代码执行输出结果为:
26 RUNOOB: 5 Runoob: 10 Swift: 11 ["RUNOOB", "Runoob", "Swift"] [5, 10, 11]
直接写 for (key, value) in ages 得到的顺序是不确定的,上面第一段代码只累加求和,不依赖顺序,所以输出稳定。
sorted(by:) 把字典转成按指定规则排列的 (key, value) 数组,再遍历就固定了。
keys 与 values 返回的是视图,本身也没有顺序保证,通常要再套一层 sorted() 使用。
注意:不要依赖观察到的「插入顺序」。字典内部用哈希表存储,元素的排列取决于哈希值和当前容量,扩容后还可能变化。
mapValues 与 merging
mapValues 只变换值,键保持不变,返回一个新字典。
实例
let ages = ["Runoob": 12, "Swift": 11]
// mapValues 只变换值,保留原来的键
let doubled = ages.mapValues { $0 * 2 }
for (key, value) in doubled.sorted(by: { $0.key < $1.key }) {
print("\(key): \(value)")
}
以上代码执行输出结果为:
Runoob: 24 Swift: 22
merging(_:uniquingKeysWith:) 把另一个字典合并进来,键冲突时由闭包决定保留哪个值。
实例
let ages = ["Runoob": 10, "RUNOOB": 5]
let extra = ["Swift": 100, "Runoob": 99]
// 键冲突时,由闭包决定保留哪一个;这里返回新值
let merged = ages.merging(extra) { current, new in new }
for (key, value) in merged.sorted(by: { $0.key < $1.key }) {
print("\(key) => \(value)")
}
以上代码执行输出结果为:
RUNOOB => 5 Runoob => 99 Swift => 100
闭包的第一个参数是原字典里的值,第二个参数是新字典里的值,返回谁就保留谁。
上例中 Runoob 在两个字典里都有,闭包返回 new,所以结果是 99;改成 current 则会保留 10。
不需要改原字典时用 merging,需要就地合并则用 merge(_:uniquingKeysWith:)。
键的要求
键必须遵循 Hashable 协议,因为字典要靠哈希值定位元素。
字符串、整数、浮点数、布尔、枚举等常见类型都满足这个要求,自定义结构体也能自动合成实现。
实例
// 让自定义结构体遵循 Hashable,就可以作为字典的键
struct Site: Hashable {
let name: String
}
var visits: [Site: Int] = [:]
visits[Site(name: "Runoob")] = 10
visits[Site(name: "RUNOOB")] = 5
print(visits[Site(name: "Runoob")] ?? 0)
print(visits.count)
以上代码执行输出结果为:
10 2
Hashable 继承自 Equatable,所以作为键的类型也必须能判断相等。
结构体的成员都是 Hashable 时,编译器会自动合成 Hashable 与 Equatable 的实现,不需要手写。
注意:类的实例默认按「引用」比较,不会自动按属性合成哈希。如果要把类当作键,需要自己实现 Hashable 与 ==。
常见问题
下面是字典使用中最常见的几个问题。
为什么字典遍历顺序每次都不一样
字典内部用哈希表存储,元素的排列取决于哈希值和容量,Swift 不承诺任何顺序。
需要顺序时请显式排序,例如 ages.sorted(by: { $0.key < $1.key }),不要依赖观察到的「插入顺序」。
字典是值类型还是引用类型
是值类型。赋值给另一个变量后修改其中一个,另一个不受影响,和数组一样由写时复制优化。
这与 Objective-C 中共享同一个对象的 NSDictionary 完全不同。
计数场景怎么写得简洁
用 default 下标,counts[word, default: 0] += 1 一行就能完成「不存在则初始化,存在则累加」。
换成普通下标要先写 if let 判断,再赋值回去,代码会长不少。
键必须是什么类型
必须遵循 Hashable,绝大多数标准库类型都满足。
自定义结构体或枚举作为键时,让它们遵循 Hashable 即可,Swift 能自动合成实现。
