Swift Dictionary 数据结构
Dictionary 是除了 Array 之外的另一种非常重要的数据结构,它用于把某种形式的 key,关联到某种形式的 value。我们来看一个例子。
定义 Dictionary
假设我们要定义一个数据结构,用来保存用户在泊学对某个视频的观看情况。可以这样:
假设我们要定义一个数据结构,用来保存用户在泊学对某个视频的观看情况。可以这样: |
在上面代码里,我们用 [KeyType: ValueType] 的形式来定义一个 Dictionary。当定义好 Dictionary 之后,我们就能直接用 [Key] 来访问某个 key 对应的值了:
record11["uid"] // number(11) |
上面例子中的结果都很直观。但是有一个细节却是值得我们注意的。和 Array 不同的是,[] 用在 Dictionary 的时候,会返回一个 Optional 类型来确保这种形式的访问安全。因此,访问不存在的 key,并不会导致运行时错误。
这是因为索引这个概念,对 Array 和 Dictionary 来说,是截然不同的。对于 Array 来说,我们有可能使用的正常索引值只源于 Array 自身,也就是 0..<array.count,因此,如果你使用了不在这个范围里的值,则一定是可以被定性为 Bug 的,何况,我们之前也看到了,对于 Array,我们几乎不需要直接使用索引来访问元素。
而对于 Dictionary 来说,它包含的内容并不直接决定我们可以查询的内容。举个例子来说,英汉词典中也可能并不包含我们要查询的单词。所以,Dictionary 中包含的所有键值,从语义上说,并不完全决定了它的使用者会查询的值,所以,我们也无法把这类问题明确的归因于是 Bug。所以,Swfit 为 Dictionary 的索引查询操作,提供了 optional 保护。要么得到正确的结果,要么通过 nil 表示要查询的内容不存在。
常用的基本属性
作为一个集合类型,Dictionary 同样有 count 和 isEmpty 两个属性读取其元素的个数以及判断其是否为空:
record11.count // 4 |
另外,我们可以单独访问一个 Dictionary 的所有 keys 和所有 values:
record11.keys |
这两个属性也分别是一个集合,我们可以暂时忽略掉它们具体的类型,如果要我们要访问它们的每一个元素,直接用 for 循环或 forEach 遍历就好了:
for key in record11.keys { print(key) } |
添加、更新和删除元素
和 Array 一样,Dictionary 也是一个值类型,当我们复制 Dictionary 对象的时候,就会拷贝 Dictionary 中的所有内容:
var record10 = record11 |
并且,直接使用 key 就可以访问和修改 Dictionary 的内容:
record10["favourite"] = .bool(false) // false |
如果我们希望更新 value 的时候,同时获得修改前的值,还可以使用 updateValue (_:forKey:) 方法:
record10.updateValue(.bool(true), |
从上面的结果可以看出修改 record10 并不会影响 record11。
当我们要在 Dictionary 中添加元素时,直接给要添加的 key 赋值就好了:
record10["watchLater"] = .bool(false) |
这样,record10 中的内容,就变成了 5 项。而当我们要删除特定的 key 时,直接把它的值设置为 nil:
record10["watchLater"] = nil |
这里,并不是把特定 key 的值设置为 nil(毕竟 Dictionary 中 value 部分的类型也不是 optional),而是删除特定的 key。当某个 key 的 value 被设置成 nil 后,这个 key 也就从 Dictionary 中删除了。
遍历 Dictionary
由于 Dictionary 同时包含了 key 和 value,因此,我们也有多重方式来遍历 Dictionary。最简单的,就是遍历 Dictionary 中的每一个元素:
for (k, v) in record10 { |
从上面的例子可以看到,遍历 Dictionary 和遍历 Array 是类似的。当我们使用 for 循环遍历时,它的每一个元素都用一个 tuple 来表示,封装了每一个元素的 key 和 value。而当使用 forEach 方法时,它会给它的 closure 参数传递两个值,分别是每一个元素的 key 和 value。
[^_^]: # (但是,由于 Dictionary 是一个无序集合(unordered collection),因此当我们编辑了 Dictionary 之后,每次遍历,访问元素的顺序都可能是不同的。如果我们希望按照固定的顺序来访问 Dictionary 中的元素,一个最简单的办法,就是对 key 排序后,再进行遍历:)
[^_^]: # (for key in record10.keys.sorted() {print(“(key): (record10[key])”)})
合并进不同的设置
enum RecordType { |
这样,当创建新纪录时,我们希望保持默认记录中的默认值,同时合并进不同用户的设置,例如:
var template = defaultRecord |
merge
然而,该如何实现这个 merge 呢?最重要的事情,就是要想一下什么内容可以被 merge 进来。最一般的情况来说,无论任何形式的序列,只要它的元素中 key 和 value 的类型和 Dictionary 相同,就可以进行合并。
如何在代码中表达这个特征呢?来看下面的例子:
extension Dictionary { |
由于 Dictionary 是一个 struct,并且 merge 修改了 self,我们必须使用 mutating 关键字修饰这个方法。而对于 sequence 参数,我们通过 where 关键字限定了两个内容:
- S 必须遵从 Sequence protocol,Dictionary 是众多遵从了 Sequence protocol 的 collection 类型之一,但是,我们没必要一定只能合并 Dictionary;
- S 的元素类型必须和原 Dictionary 的 Element 相同,其中 Key 和 Value 是 Dictionary 声明中的两个泛型参数;
解决了参数问题之后,实现合并的算法就很简单了,我们只是更新 self 中每一个和 sequence 有相同 key 的值就好了。
这样,之前 template.merge (record11Patch) 就可以正常工作了。
既然,我们把 merge 参数的约束定义为了 Sequence,那我们就来看一个合并非 Dictionary 类型的情况,例如,合并一个包含正确内容的 Array:
let record10Patch: [(key: String, value: RecordType)] = [ |
在上面的代码里,我们合并了一个 tuple 数组,它的类型是 Array<String, RecordType>,数组中的每一项都包含了一个要合并进来的键值对。如果没有意外,合并 Array 和 Dictionary 都应该是可以正常工作的。
按照我们对 merge 的实现方式,实际上,任何一个遵从了 Sequence protocol 的类型,只要它包含了和 template 相同的元素类型,都是可以 merge 的。
用一个 tuple 数组初始化 Dictionary
理解了 merge 的实现和用法之后,其实,我们很容易把这个场景进一步扩展下,如果我们可以 merge 类型兼容的 Sequence,那么,用这样的 Sequence 来初始化一个 Dictionary 也是可以的,把它看成是和一个空的 Dictionary 进行合并就好了:
extension Dictionary { |
有了这个方法之后,我们直接用下面的代码就可以创建一个新的 Dictionary 对象:
let record11 = Dictionary(record11Patch) |
定制 map 的行为
最后一个要介绍的常用功能,是定制 Dictionary.map 的行为,默认情况下它返回的是一个 Array,例如:
record11.map { $1 } |
在上面的例子里,map 返回一个 Array
们可以自定义一个“只map value”的方法: |
在这个实现的最内部,我们用标准库中的 map 得到了一个 Array<(String, RecordType)> 类型的 Array,而后,由于 Array 也遵从了 Sequence protocol,因此,我们就能直接使用这个 Array 来定义新的 Dictionary 了。
完成之后,用下面的代码测试下:
let newRecord11 = record11.mapValue { record -> String in |