Dictionary是除了Array之外的另一种非常重要的数据结构,它用于把某种形式的key,关联到某种形式的value。我们来看一个例子。

定义Dictionary

假设我们要定义一个数据结构,用来保存用户在泊学对某个视频的观看情况。可以这样:

假设我们要定义一个数据结构,用来保存用户在泊学对某个视频的观看情况可以这样:

enum RecordType {
case bool(Bool)
case number(Int)
case text(String)
}

let record11: [String: RecordType] = [
"uid": .number(11),
"exp": .number(100),
"favourite": .bool(true),
"title": .text("Dictionary basics")
]

在上面代码里,我们用[KeyType: ValueType]的形式来定义一个Dictionary。当定义好Dictionary之后,我们就能直接用[Key]来访问某个key对应的值了:

record11["uid"]       // number(11)
record11["favourite"] // bool(true)
record11["title"] // text("Dictionary basics")
record11["invalid"] // nil

// Optional<RecordType>.Type
type(of: record11["favourite"])

上面例子中的结果都很直观。但是有一个细节却是值得我们注意的。和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
record11.isEmpty // false

另外,我们可以单独访问一个Dictionary的所有keys和所有values:

record11.keys
record11.values

这两个属性也分别是一个集合,我们可以暂时忽略掉它们具体的类型,如果要我们要访问它们的每一个元素,直接用for循环或forEach遍历就好了:

for key in record11.keys { print(key) }
// or
record11.keys.forEach { print($0) }

添加、更新和删除元素

和Array一样,Dictionary也是一个值类型,当我们复制Dictionary对象的时候,就会拷贝Dictionary中的所有内容:

var record10 = record11

并且,直接使用key就可以访问和修改Dictionary的内容:

record10["favourite"] = .bool(false) // false
record11["favourite"] // true

如果我们希望更新value的时候,同时获得修改前的值,还可以使用updateValue(_:forKey:)方法:

record10.updateValue(.bool(true),
forKey: "favourite") // .bool(false)

从上面的结果可以看出修改record10并不会影响record11。

当我们要在Dictionary中添加元素时,直接给要添加的key赋值就好了:

record10["watchLater"] = .bool(false)
// [
// "favourite": RecordType.bool(false),
// "exp": RecordType.number(100),
// "title": RecordType.text("Directory basics"),
// "uid": RecordType.number(11),
// "watchLater": RecordType.bool(false)
// ]

这样,record10中的内容,就变成了5项。而当我们要删除特定的key时,直接把它的值设置为nil:

record10["watchLater"] = nil
// [
// "favourite": RecordType.bool(false),
// "exp": RecordType.number(100),
// "title": RecordType.text("Directory basics"),
// "uid": RecordType.number(11)
// ]

这里,并不是把特定key的值设置为nil(毕竟Dictionary中value部分的类型也不是optional),而是删除特定的key。当某个key的value被设置成nil后,这个key也就从Dictionary中删除了。

遍历Dictionary

由于Dictionary同时包含了key和value,因此,我们也有多重方式来遍历Dictionary。最简单的,就是遍历Dictionary中的每一个元素:

for (k, v) in record10 {
print("\(k): \(v)")
}
//or
record10.forEach { print("\($0): \($1)") }

从上面的例子可以看到,遍历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 {
case bool(Bool)
case number(Int)
case text(String)
}

let defaultRecord: [String: RecordType] = [
"uid": .number(0),
"exp": .number(100),
"favourite": .bool(false),
"title": .text("")
]

这样,当创建新纪录时,我们希望保持默认记录中的默认值,同时合并进不同用户的设置,例如:

var template = defaultRecord
var record11Patch: [String: RecordType] = [
"uid": .number(11),
"title": .text("Common dictionary extensions")
]

// How can we do this?
// template.merge(record11Patch)
// [
// uid: .number(11),
// "exp": .number(100),
// "favourite": .bool(false),
// "title": .text("Common dictionary extensions")
// ]

merge

然而,该如何实现这个merge呢?最重要的事情,就是要想一下什么内容可以被merge进来。最一般的情况来说,无论任何形式的序列,只要它的元素中key和value的类型和Dictionary相同,就可以进行合并。

如何在代码中表达这个特征呢?来看下面的例子:

extension Dictionary {
mutating func merge<S:Sequence>(_ sequence: S)
where S.Iterator.Element == (key: Key, value: Value) {

sequence.forEach { self[$0] = $1 }
}
}

由于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)] = [
(key: "uid", value: .number(10)),
(key: "title", value: .text("Common dictionary extensions"))
]

var template1 = defaultRecord
template1.merge(record10Patch)
// [
// uid: .number(10),
// "exp": .number(100),
// "favourite": .bool(false),
// "title": .text("Common dictionary extensions")
// ]

在上面的代码里,我们合并了一个tuple数组,它的类型是Array<String, RecordType>,数组中的每一项都包含了一个要合并进来的键值对。如果没有意外,合并Array和Dictionary都应该是可以正常工作的。

按照我们对merge的实现方式,实际上,任何一个遵从了Sequence protocol的类型,只要它包含了和template相同的元素类型,都是可以merge的。

用一个tuple数组初始化Dictionary

理解了merge的实现和用法之后,其实,我们很容易把这个场景进一步扩展下,如果我们可以merge类型兼容的Sequence,那么,用这样的Sequence来初始化一个Dictionary也是可以的,把它看成是和一个空的Dictionary进行合并就好了:

extension Dictionary {
init<S:Sequence>(_ sequence: S)
where S.Iterator.Element == (key: Key, value: Value) {

self = [:]
self.merge(sequence)
}
}

有了这个方法之后,我们直接用下面的代码就可以创建一个新的Dictionary对象:

let record11 = Dictionary(record11Patch)
// [
// uid: .number(11),
// "title": .text("Common dictionary extensions")
// ]

定制map的行为

最后一个要介绍的常用功能,是定制Dictionary.map的行为,默认情况下它返回的是一个Array,例如:

record11.map { $1 }
// [ .number(11).text("Common dictionary extensions")]

在上面的例子里,map返回一个Array,但有时,我们仅仅希望对value做一些变换,而仍旧保持Dictionary的类型。为此,我们可以自定义一个“只map value”的方法:

们可以自定义一个“只map value”的方法:

extension Dictionary {
func mapValue<T>(_ transform: (Value) -> T) -> [Key: T] {
return Dictionary<Key, T>(map { (k, v) in
return (k, transform(v))
})
}
}

在这个实现的最内部,我们用标准库中的map得到了一个Array<(String, RecordType)>类型的Array,而后,由于Array也遵从了Sequence protocol,因此,我们就能直接使用这个Array来定义新的Dictionary了。

完成之后,用下面的代码测试下:

let newRecord11 = record11.mapValue { record -> String in
switch record {
case .text(let title):
return title
case .number(let exp):
return String(exp)
case .bool(let favourite):
return String(favourite)
}
}

// [
// "uid": "11",
// "title": "Common dictionary extensions"
// ]
这样,我们就用record11生成了一个Dictionary<String, String>类型的对象