Swift 4.0 中字典和集合的改进

在最新发布的 Swift 版本中,字典和集合获得了一些新的方法和初始化器,使得常见的任务比以往任何时候都更容易。诸如分组、过滤和转换值等操作现在可以在一个步骤中完成,让您编写更具表现力和效率的代码。

这篇文章探讨了这些新的转换,并以一个市场的杂货数据为例。这个自定义的 GroceryItem 结构体,由名称和部门组成,将作为数据类型

struct GroceryItem: Hashable {
    var name: String
    var department: Department

    enum Department {
        case bakery, produce, seafood
    }

    static func ==(lhs: GroceryItem, rhs: GroceryItem) -> Bool {
        return (lhs.name, lhs.department) == (rhs.name, rhs.department)
    }

    var hashValue: Int {
        // Combine the hash values for the name and department
        return name.hashValue << 2 | department.hashValue
    }
}

// Create some groceries for our store:
let 🍎 = GroceryItem(name: "Apples", department: .produce)
let 🍌 = GroceryItem(name: "Bananas", department: .produce)
let 🥐 = GroceryItem(name: "Croissants", department: .bakery)
let 🐟 = GroceryItem(name: "Salmon", department: .seafood)
let 🍇 = GroceryItem(name: "Grapes", department: .produce)
let 🍞 = GroceryItem(name: "Bread", department: .bakery)
let 🍤 = GroceryItem(name: "Shrimp", department: .seafood)

let groceries = [🍎, 🍌, 🥐, 🐟, 🍇, 🍞, 🍤]

以下示例使用 groceries 数组来构建和转换使用这些新工具的字典。

按键对值进行分组

Grouping groceries by their department

一个新的分组初始化器使得从一系列值构建字典变得轻而易举,这些值按照从这些值计算出的键进行分组。我们将使用这个新的初始化器来构建一个按部门分组的杂货字典。

在早期版本的 Swift 中,您使用迭代从头开始构建字典。这需要类型注释、手动迭代以及检查每个键是否已存在于字典中。

// Swift <= 3.1
var grouped: [GroceryItem.Department: [GroceryItem]] = [:]
for item in groceries {
    if grouped[item.department] != nil {
        grouped[item.department]!.append(item)
    } else {
        grouped[item.department] = [item]
    }
}

随着 Swift 的这次更新,您可以使用 Dictionary(grouping:by) 初始化器,用一行代码创建相同的字典。传递一个闭包,该闭包为数组中的每个元素返回一个键。在以下代码中,闭包为每个杂货项返回部门

// Swift 4.0
let groceriesByDepartment = Dictionary(grouping: groceries,
                                       by: { item in item.department })
// groceriesByDepartment[.bakery] == [🥐, 🍞]

生成的 groceriesByDepartment 字典为杂货列表中的每个部门都有一个条目。每个键的值是该部门内的杂货数组,顺序与原始列表相同。在 groceriesByDepartment 中使用 .bakery 作为键,您将得到数组 [🥐, 🍞]

转换字典的值

您可以使用新的 mapValues(_:) 方法来转换字典的值,同时保持相同的键。此代码将 groceriesByDepartment 中的项目数组转换为它们的计数,从而为每个部门中的项目数量创建一个查找表

let departmentCounts = groceriesByDepartment.mapValues { items in items.count }
// departmentCounts[.bakery] == 2

因为字典具有所有相同的键,只是值不同,所以它可以使用与原始字典相同的内部布局,并且不需要重新计算任何哈希值。这使得调用 mapValues(_:) 比从头开始重建字典更快。

从键值对构建字典

Building a dictionary from names and values

您现在可以使用两个不同的初始化器从键值对序列创建字典:一个用于您拥有唯一键时,另一个用于您可能拥有重复键时。

如果您从键序列和值序列开始,则可以使用 zip(_:_:) 函数将它们组合成一个对序列。例如,此代码创建一个元组序列,其中包含杂货项的名称和项目本身

let zippedNames = zip(groceries.map { $0.name }, groceries)

zippedNames 的每个元素都是一个 (String, GroceryItem) 元组,其中第一个是 ("Apples", 🍎)。因为每个杂货项都有一个唯一的名称,所以以下代码成功创建了一个使用名称作为杂货项键的字典

var groceriesByName = Dictionary(uniqueKeysWithValues: zippedNames)
// groceriesByName["Apples"] == 🍎
// groceriesByName["Kumquats"] == nil

仅当您确定数据具有唯一键时,才使用 Dictionary(uniqueKeysWithValues:) 初始化器。序列中的任何重复键都将触发运行时错误。

如果您的数据有(或可能具有)重复的键,请使用新的合并初始化器 Dictionary(_:uniquingKeysWith:)。此初始化器接受一个键值对序列,以及一个在键重复时调用的闭包。唯一化闭包将共享同一键的第一个和第二个值作为参数,并且可以返回现有值、新值或以您决定的任何方式组合它们。

例如,以下代码通过使用 Dictionary(_:uniquingKeysWith:)(String, String) 元组数组转换为字典。请注意,"dog" 是两个键值对中的键。

let pairs = [("dog", "🐕"), ("cat", "🐱"), ("dog", "🐶"), ("bunny", "🐰")]
let petmoji = Dictionary(pairs,
                         uniquingKeysWith: { (old, new) in new })
// petmoji["cat"] == "🐱"
// petmoji["dog"] == "🐶"

当到达键为 "dog" 的第二个键值对时,将使用旧值和新值("🐕""🐶")调用唯一化闭包。由于闭包始终返回其第二个参数,因此结果将 "🐶" 作为 "dog" 键的值。

选择特定条目

字典现在有一个 filter(_:) 方法,该方法返回一个字典,而不仅仅是键值对数组,就像早期版本的 Swift 中一样。传递一个闭包,该闭包将键值对作为其参数,如果该对应该在结果中,则返回 true

func isOutOfStock(_ item: GroceryItem) -> Bool {
    // Looks up `item` in inventory
}

let outOfStock = groceriesByName.filter { (_, item) in isOutOfStock(item) }
// outOfStock["Croissants"] == 🥐
// outOfStock["Apples"] == nil

此代码对每个项目调用 isOutOfStock(_:) 函数,仅保留缺货的杂货项。

使用默认值

字典现在有了第二个基于键的下标,可以更轻松地获取和更新值。以下代码定义了一个简单的购物车,实现为一个项目及其计数的字典

// Begin with a single banana
var cart = [🍌: 1]

由于某些键可能在字典中没有对应的值,因此当您使用键查找值时,结果是可选的。

// One banana:
cart[🍌]    // Optional(1)
// But no shrimp:
cart[🍤]    // nil

您现在可以使用带有键和 default 参数的下标来索引字典,而不是使用 nil 合并运算符 (??) 将可选值转换为您需要的实际计数。如果找到键,则返回其值,并忽略默认值。如果未找到键,则下标返回您提供的默认值。

// Still one banana:
cart[🍌, default: 0]    // 1
// And zero shrimp:
cart[🍤, default: 0]    // 0

您甚至可以通过新的下标修改值,从而简化向购物车添加新项目所需的代码。

for item in [🍌, 🍌, 🍞] {
    cart[item, default: 0] += 1
}

当此循环处理每个香蕉 (🍌) 时,检索当前值,递增,然后存储回字典中。当需要添加面包 (🍞) 时,字典找不到键,而是返回默认值 (0)。在该值递增后,字典添加新的键值对。

在循环结束时,cart[🍌: 3, 🍞: 1]

将两个字典合并为一个

除了更轻松的增量更改之外,字典现在还简化了批量更改,使用方法可以将一个字典合并到另一个字典中。

Merging two carts together

要合并 cart 和另一个字典的内容,您可以使用可变 merge(_:uniquingKeysWith:) 方法。您传递的唯一化闭包的工作方式与 Dictionary(_:uniquingKeysWith:) 初始化器中的工作方式相同:每当有两个具有相同键的值时调用它,并返回一个、另一个或值的组合。

在此示例中,将加法运算符作为 uniquingKeysWith 参数传递会将任何匹配键的计数相加,因此更新后的购物车具有每个项目的正确总数

let otherCart = [🍌: 2, 🍇: 3]
cart.merge(otherCart, uniquingKeysWith: +)
// cart == [🍌: 5, 🍇: 3, 🍞: 1]

要创建一个包含合并内容的新字典,而不是就地合并,请使用非可变 merging(_:uniquingKeysWith:) 方法。

还有更多…

我们还有一些其他新增功能没有介绍。字典现在具有自定义的 keysvalues 集合,具有新的功能。keys 集合保持快速的键查找,而可变的 values 集合允许您就地修改值。

与字典一样,集合也获得了一个新的 filter(_:) 方法,该方法返回相同类型的集合,而不是像早期版本的 Swift 中的数组。最后,集合和字典现在都公开了它们的当前容量,并添加了 reserveCapacity(_:) 方法。通过这些新增功能,您可以查看和控制其内部存储的大小。

除了自定义的 keysvalues 集合之外,所有这些更改在 Swift 3.2 中都可用。即使您尚未切换到使用 Swift 4.0,您也可以立即开始利用这些改进!

您可以在 DictionarySet 文档中找到有关所有这些新功能的更多信息,或者在 Swift Evolution 提案中阅读有关新增功能背后的原理,这些提案分别针对 自定义的 keysvalues 集合其他字典和集合增强功能