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

Swift 字典

字典(Dictionary)用来按键值对存放元素,通过键快速找到对应的值,查找过程与元素数量无关。

它和数组一样是值类型,赋值和传参时保持独立;但它不保证顺序,这一点和数组正好相反。

本篇讲字典的创建、增删改、default 下标、遍历、mapValues 与 merging,以及键的 Hashable 要求。

按位置存放元素请见《Swift 数组》。


创建字典

字典的类型写作方括号里放「键类型: 值类型」,比如 [String: Int]。

字面量用 键: 值 的形式列出,空字典有两种写法。

实例

import Foundation

// 字典字面量的类型标注是 [键类型: 值类型]
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:),它会把被删除的值作为可选值返回。

实例

import Foundation

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 的下标在键不存在时直接给出默认值,并且允许就地修改。

实例

import Foundation

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 会编译不过,因为不能对可选值做加法。


遍历字典

字典是无序集合,遍历顺序不保证,也不等于插入顺序,更不能跨版本、跨平台地假设它稳定。

需要稳定输出时,先对键排序再遍历。

实例

import Foundation

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 只变换值,键保持不变,返回一个新字典。

实例

import Foundation

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:) 把另一个字典合并进来,键冲突时由闭包决定保留哪个值。

实例

import Foundation

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 协议,因为字典要靠哈希值定位元素。

字符串、整数、浮点数、布尔、枚举等常见类型都满足这个要求,自定义结构体也能自动合成实现。

实例

import Foundation

// 让自定义结构体遵循 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 能自动合成实现。