Dictionary初始化以及常用操作的诸多改进

在SE-0165这份提议中,针对DictionarySet的常用场景,为这两种类型在初始化、内容读写以及成员遍历方面都做了一些改进。在这一节,我们就来逐一了解它们。

改进的init方法

当我们对ArraySet进行过一些函数式操作后,得到的结果有可能会丢掉之前的集合类型。例如:

let numberSet = Set(1...100)
let evens = numberSet.lazy.filter { $0 % 2 == 0 }
type(of: evens) // LazyFilterCollection>

尽管概念上,我们认为evens仍旧应该是一个Set,但实际上,它是一个LazyFilterCollectionArray的情况是类似的,我们就不列举了)。于是,evens就不再支持Set的所有操作了。于是,下面的代码尽管在语义上正确,但会导致编译错误:

evens.isSubset(of: numberSet) // !! ERROR !!

为了解决类似的问题,ArraySetinit方法可以把这种函数式操作后的结果,再变回各自标准的类型,像这样:

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重复了。对此,我们需要给Dictionaryinit方法提供一个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"]

你可能感兴趣的:(Dictionary初始化以及常用操作的诸多改进)