嵌套结构难以阅读;管道stream通常更容易阅读和思考。
嵌套结构具有“厄运之箭”的感觉,您需要同时管理所有父结构的上下文;而管道stream通常是线性的。
许多语言都添加了“函数式管道”风格,建立在首先在 Lisp 中探索的 map-filter-reduce 的基础上,哦,大约 50 年前,大约 40 年前在 Unix 和 Smalltalk 中:)
在下面描述的内容在概念上适用于 Java、C#、Kotlin、Python 等。我将使用 Swift 中的示例,并解释任何特定于 Swift 的构造。
我使用所有这三种方法:
- 工具:如果您的工具可以胜任,请使用该工具!例如,当 IntelliJ IDEA 看到一个知道如何转换的循环时,它会弹出一个黄色的灯泡,提供执行此操作。
- 提取新集合:当循环遍历集合时,将集合提取到变量中作为新管道的种子,并逐渐将循环的部分移入其中,直到原始循环消失。Martin Fowler 在他的优秀书籍和文章(请参阅参考资料)中探讨了这种方法,因此我不会进一步探讨。
- 就地转换:将循环转换为就地管道,一次一个嵌套级别。我们将在下面使用这种方法。
让我们来看一个例子。我们将一步一步地将下面的循环变成一个函数性管道。该集合是一个数组,包含字典(Map)。
var points = [ ["x":"17", "y":"23"], ["x": "x12", "y": "y100"], ["x": "3", " y": "2", "z": "11"], ["w":"21"]] |
目标是计算任何具有 y 坐标的条目的平均值。
var sum = 0 |
print(sum / count) |
使用 forEach() 替代for循环:
var sum = 0 |
使用compactMap() 处理条件不满足的情况:
var sum = 0
var count = 0
points
.compactMap { $0["y"] }
.forEach { y in
if let theInt = Int(y) {
sum += theInt
count += 1
}
}
print(sum / count)
再次使用 compactMap()忽略格式错误的整数
var sum = 0 |
路径 1:拆分循环
我们可能会看到并意识到我们的代码正在计算两件事,并拆分循环。要做到这一点,我们将保存常用的计算成一个新的集合,独立重复迭代处理sum和count。
let values = points |
在两个循环中使用reduce()
let values = points |
我们可以更轻松地计算计数:
let count = values.count
路径 2:单管道
我们可能会认识到 sum 和 count 可以保存在一个元组中(一个主要是匿名的对象):
var tuple = (sum: 0, count: 0) |
如果你像我一样,这个元组看起来有点难看,而且可能令人困惑。我会省去你使用它的 reduce() 调用。相反,我们看到 sum 和 count 必须一起协调。听起来像是放置实际对象的好地方:
class Average { |
现在可以使用reduce :
let average = points |
两条路径的比较
当它澄清代码时,拆分循环可能是一个很好的举措(并且可能让您在多个对象之间重新分配行为)但是它有一个缺点——如果您将部分工作存储在一个集合中,您可能会强制一个真正的集合存在,使用需要的所有内存管理。
相比之下,将其保留为管道意味着可能永远不会有集合。当然,我们的示例有一个数组常量,但相同的管道适用于对象流,从不需要同时使用它们。
在这种情况下,新对象对我来说是胜利。我没想到,但是这个小对象确实改进了代码。
结论
函数式管道通常胜过嵌套结构(while - if - while -if -if 等:)。
- 更容易理解:您需要维护的上下文更少。
- 潜在的内存效率更高:管道在处理流时与在集合上工作一样愉快。
- 更容易并行化:您可以将每个阶段想象成自己的计算机。(不幸的是,现实生活中的并行化比这更难。)
具有这些管道的语言之间有很多重叠:它们通常提供相似的功能,即使它们稍微更改了名称。