在SE-0165这份提议中,针对Dictionary
和Set
的常用场景,为这两种类型在初始化、内容读写以及成员遍历方面都做了一些改进。在这一节,我们就来逐一了解它们。
改进的init方法
当我们对Array
或Set
进行过一些函数式操作后,得到的结果有可能会丢掉之前的集合类型。例如:
let numberSet = Set(1...100)
let evens = numberSet.lazy.filter { $0 % 2 == 0 }
type(of: evens) // LazyFilterCollection>
尽管概念上,我们认为evens
仍旧应该是一个Set
,但实际上,它是一个LazyFilterCollection
(Array
的情况是类似的,我们就不列举了)。于是,evens
就不再支持Set
的所有操作了。于是,下面的代码尽管在语义上正确,但会导致编译错误:
evens.isSubset(of: numberSet) // !! ERROR !!
为了解决类似的问题,Array
和Set
的init
方法可以把这种函数式操作后的结果,再变回各自标准的类型,像这样:
let evenSet = Set(evens)
然后,我们就可以愉快的使用操作后的结果了:
evenSet.isSubset(of: numberSet) // true
但是,Dictionary
类型的init
方法,却没有这个功效,来看下面的例子:
let numberDictionary =
["one": 1, "two": 2, "three": 3, "four": 4]
let evenColl =
numberDictionary.lazy.filter { $0.1 % 2 == 0 }
类似的,evenColl
的类型是LazyFilterCollection
,它同样不再是一个Dictionary
。但我们却无法用init
方法把这个结果转换回来:
let evenDictionary = Dictionary(evenColl) // !! ERROR !!
转回标准Dictionary的方法
为了解决这个问题,Swift 4中给Dictionary
新增了一个init
方法:
// Still unfinished, you will get a compile time error:
// generic parameter 'Key' could not be inferred.
// See https://bugs.swift.org/browse/SR-922
let evenDictionary =
Dictionary(uniqueKeysWithValues: evenColl)
但至少在录制这段视频的时候,这个功能仍旧未完全实现,SR-922记录了这个问题。对此,一个临时解决方案是这样的:
let evenDictionary = Dictionary(uniqueKeysWithValues:
evenColl.map { (key: $0.0, value: $0.1) })
// ["four": 4, "two": 2]
无论如何,理解这个init
的方法的来由,要比现在通过编译重要的多。除此之外,我们还可以更多方式来体验这个init(uniqueKeysWithValues:)
方法。例如,把一个Array
变成用其索引为Key的Dictionary
:
let numbers = ["ONE", "TWO", "THREE"]
var numbersDict = Dictionary(uniqueKeysWithValues:
numbers.enumerated().map { ($0.0 + 1, $0.1) })
// [2: "TWO", 3: "THREE", 1: "ONE"]
当然,用我们之前提过单边range操作符,上面的代码还可以写成这样:
numbersDict = Dictionary(uniqueKeysWithValues:
zip(1..., numbers))
// [2: "TWO", 3: "THREE", 1: "ONE"]
总之,用一句话总结就是,init(uniqueKeysWithValues:)
的目的,就是把各种处理过后的集合类型,重新变回标准Dictionary
。
解决源数据中的重复Key
在之前把其它集合转换回Dictionary
的例子里,我们忽略掉了一个问题。如果源数据中作为Key的部分有重复怎么办呢,例如这样:
let duplicates = [("a", 1), ("b", 2), ("a", 3), ("b", 4)]
// fatal error: Duplicate values for key: 'a'
let letters = Dictionary(uniqueKeysWithValues: duplicates)
我们手工构造了一个包含重复Key的Array<(String, Int)>
,这时编译器就会提示我们作为Key,a
重复了。对此,我们需要给Dictionary
的init
方法提供一个clousre,告诉它重复Key的处理方法。
例如,只选择第一个遇到的Key和Value:
let letters = Dictionary(duplicates,
uniquingKeysWith: { (first, _) in first })
// ["b": 2, "a": 1]
这里,uniquingKeysWith
的类型是(Value, Value) throws -> Value
,表示如何处理Key相同时的两个Value。在上面的例子里,我们执行的动作就是只选择第一个。
理解了这个closure的含义之后,我们就可以对重复Key的值采取各种行动了,例如,选择相同Key中的最大一个:
let letters = Dictionary(duplicates, uniquingKeysWith: max)
// ["b": 4, "a": 3]
组织序列中满足特定条件的元素
假设我们有一个记录人名的数组:
let names = ["Aaron", "Abe", "Bain", "Bally", "Bald", "Mars", "Nacci"]
为了按起始字母分类所有人名,我们可以这样:
let groupedNames = Dictionary(grouping: names, by: { $0.first! })
// ["B": ["Bain", "Bally", "Bald"], "A": ["Aaron", "Abe"], "M": ["Mars"], "N": ["Nacci"]]
带有默认值的下标操作符
在Dictionary
里,使用下标操作符会返回Value?
而不是Value
在某些时候是个很麻烦的事情。例如,我们用一个Dictionary
统计字符串中每个字符的出现的个数:
let characters = "aaabbbcc"
var frequencies: [Character: Int] = [:]
characters.forEach {
if frequencies[$0] != nil {
frequencies[$0]! += 1
}
else {
frequencies[$0] = 1
}
}
frequencies
// ["b": 3, "a": 3, "c": 2]
在这个例子里,我们得通过一个if
判断下标操作符是否为nil
,来为还没统计过的字符设置默认值1。为了进一步改进这种应用场景的语义,Swift 4为Dictionary
的下标添加了默认值:
characters.forEach {
frequencies[$0, default: 0] += 1
}
这样,只要在下标操作符中使用了default
,对Dictionary
的访问就不再返回optional了,就语义来说,也更易懂。
转为Dictionary定制的filter和mapValue
在Swift 3中,对Dictionary
调用filter
会返回一个Array<(Key, Value)>
,但在Swift 4里,返回的结果,仍旧是和之前同样的Dictionary
:
let filtered = numberDictionary.filter { $0.value % 2 == 0 }
// Array<(Key, Value)> in Swift 3
// Dictionary in Swift 4
type(of: filtered)
另外,有时,我们仅希望对Dictionary
中的Value
进行某种变换,并保持Dictionary
的类型不变,在之前的Swift 3 Dictionary视频中,我们还提到过这种需求,现在,Swift 4官方添加了这个mapValues
方法:
let mapped = numberDictionary.mapValues { $0.lowercased() }
mapped // [2: "two", 3: "three", 1: "one"]