几乎每个Swift程序都以这种或那种方式使用集合。无论是存储要以某种形式的列表显示的值,跟踪观察者还是缓存数据 - 集合都无处不在。
使用集合时,在一系列操作中使用相同的数据集非常常见,并且不断将其转换为新值。例如,我们可能会下载一些JSON数据,然后将其转换为字典数组,最后转换为模型集合。
小编这里有大量的书籍和面试资料哦(点击下载)
映射和flatMapping
让我们从基础开始 - 使用map
和flatMap
。它们都允许我们使用函数或闭包将集合转换为另一个集合,其区别在于flatMap
自动跳过nil
值。
有趣的是,这两个转换API都rethrowing
意味着它们将自动抛出在转换过程中生成的任何错误(并且它们根本不会throwing
在转换闭包本身不存在的情况下throwing
)。
假设我们想要扩展Bundle
类,以便为我们提供一种通过名称加载文件数组的简单方法。为此,我们可以编写一个for
循环遍历每个名称的-loop,并将加载的文件收集到一个可变数组中,但有什么好处呢?😉
让我们使用map
而flatMap
不是 - 为自己建立一个很好的转换链:
extension Bundle {
func loadFiles(named fileNames: [String]) throws -> [File] {
return try fileNames
// Since flatMap returns a new sequence of all non-nil
// values returned from its closure, it lets us automatically
// skip all files that don't exist.
.flatMap({ name in
return url(forResource: name, withExtension: nil)
})
.map(Data.init)
.map(File.init)
}
}
上面我们还利用了Swift的一流函数功能(我们在2周前看了一下),通过传递Data
和File
初始化器作为map
函数的闭包。
结果是一个更具说明性的设置,我们想要对我们的集合执行的每个转换都非常清晰地定义。但是,有人可能会认为它也会对可读性造成一定的伤害,特别是对于可能不太熟悉此类概念的开发人员而言。
让我们看看我们如何可以提高这些转换的可读性,同时仍然使用标准库提供的所有内容 - 通过查看reduce
API。
减少
该reduce
方法使您可以将集合减少为单个值。而不是转换为新的集合(如使用map
和时flatMap
),最终会得到一个基于对集合中每个元素应用闭包的结果。
假设我们正在构建一个游戏,并且在每个会话结束时,我们想通过迭代Level
游戏中的所有模型并将其得分相加来计算玩家的总得分。
同样,这可以通过mutable Int
和a for
-loop 来完成,但就像使用时一样map
,flatMap
我们可以一次性执行此操作reduce
,这样可以避免任何可变状态。
有了reduce
,你传递一个初始值来开始,一个函数通过获取当前的值并转换它来返回一个新值,如下所示:
extension Game {
func calculateTotalScore() -> Int {
return levels.reduce(0) { result, level in
// On each iteration we take the previous result
// and add the current level's score.
return result + level.score
}
}
}
在标准库必须提供的所有转换API中,reduce
可以说是最难理解的,并且有时会使代码有点混乱。仅仅通过观察上面的内容,看起来我们正试图减少0,这没有多大意义😅。
我们来看看如何改进上面代码的可读性。一种方法可以是Int
在调用reduce之前将我们的级别分数映射到一个新数组,这样我们就可以简单地将+
运算符作为闭包传递(因为现在我们需要传递一个(Int, Int) -> Int
闭包,+
运算符是):
extension Game {
func calculateTotalScore() -> Int {
return levels.map({ $0.score }).reduce(0, +)
}
}
好一点,但我们仍然可以做得更好!由于(到目前为止)最常见的用例reduce
是添加数字,如何添加一个sum
函数Sequence
让我们传入一个属性,反过来将使用它reduce
,如下所示:
extension Sequence {
func sum<N: Numeric>(by valueProvider: (Element) -> N) -> N {
return reduce(0) { result, element in
return result + valueProvider(element)
}
}
}
有了上面的扩展,我们现在可以简单地传递我们想要总结的属性,为我们留下一个非常漂亮和干净的呼叫站点:
extension Game {
func calculateTotalScore() -> Int {
return levels.sum { $0.score }
}
}
太好了,对吧?😀
荏苒
最后,我们来看看如何zip
将两个序列组合成一个序列。当您有两个序列并且您不确定它们的元素数是否匹配时,这尤其有用。
假设我们想要ViewModel
在我们的一个视图中渲染,并且我们有一个ImageView
s 数组,我们将使用它来渲染一系列图像。如果我们要使用经典for
循环,我们必须进行边界检查以确保我们没有访问其中一个数组中的越界索引:
func render(_ viewModel: ViewModel) {
for (index, imageName) in viewModel.imageNames.enumerated() {
let image = UIImage(named: imageName)
// Since we might have more images than we have place for
// in the UI, we need to add this bounds-check.
guard index < imageViews.count else {
return
}
imageViews[index].image = image
}
}
如果我们改为使用zip
,我们可以免费获得边界检查。只要两个序列都有每个索引的匹配元素,迭代就会继续,所以我们可以简单地编写迭代,如下所示:
func render(_ viewModel: ViewModel) {
let images = viewModel.imageNames.flatMap(UIImage.init)
for (image, imageView) in zip(images, imageViews) {
imageView.image = image
}
}
非常干净👍
结论
对序列和集合使用函数转换可能是一把双刃剑。一方面,它可以让您大幅减少执行一系列转换所需的代码量,但另一方面,它可以使您的代码更难以阅读。
与往常一样,它成为决定何时或何时不部署此类功能的平衡行为,并且使用扩展在标准库之上添加自己的便利API也是一个很好的解决方案。
网友评论