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
数组来构建和转换使用这些新工具的字典。
按键对值进行分组
一个新的分组初始化器使得从一系列值构建字典变得轻而易举,这些值按照从这些值计算出的键进行分组。我们将使用这个新的初始化器来构建一个按部门分组的杂货字典。
在早期版本的 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(_:)
比从头开始重建字典更快。
从键值对构建字典
您现在可以使用两个不同的初始化器从键值对序列创建字典:一个用于您拥有唯一键时,另一个用于您可能拥有重复键时。
如果您从键序列和值序列开始,则可以使用 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]
。
将两个字典合并为一个
除了更轻松的增量更改之外,字典现在还简化了批量更改,使用方法可以将一个字典合并到另一个字典中。
要合并 cart
和另一个字典的内容,您可以使用可变 merge(_:uniquingKeysWith:)
方法。您传递的唯一化闭包的工作方式与 Dictionary(_:uniquingKeysWith:)
初始化器中的工作方式相同:每当有两个具有相同键的值时调用它,并返回一个、另一个或值的组合。
在此示例中,将加法运算符作为 uniquingKeysWith
参数传递会将任何匹配键的计数相加,因此更新后的购物车具有每个项目的正确总数
let otherCart = [🍌: 2, 🍇: 3]
cart.merge(otherCart, uniquingKeysWith: +)
// cart == [🍌: 5, 🍇: 3, 🍞: 1]
要创建一个包含合并内容的新字典,而不是就地合并,请使用非可变 merging(_:uniquingKeysWith:)
方法。
还有更多…
我们还有一些其他新增功能没有介绍。字典现在具有自定义的 keys
和 values
集合,具有新的功能。keys
集合保持快速的键查找,而可变的 values
集合允许您就地修改值。
与字典一样,集合也获得了一个新的 filter(_:)
方法,该方法返回相同类型的集合,而不是像早期版本的 Swift 中的数组。最后,集合和字典现在都公开了它们的当前容量,并添加了 reserveCapacity(_:)
方法。通过这些新增功能,您可以查看和控制其内部存储的大小。
除了自定义的 keys
和 values
集合之外,所有这些更改在 Swift 3.2 中都可用。即使您尚未切换到使用 Swift 4.0,您也可以立即开始利用这些改进!
您可以在 Dictionary 和 Set 文档中找到有关所有这些新功能的更多信息,或者在 Swift Evolution 提案中阅读有关新增功能背后的原理,这些提案分别针对 自定义的 keys
和 values
集合 和 其他字典和集合增强功能。