先清理代码
格式化
首先,如果格式正确,阅读/理解这段代码会容易得多。所以让我们开始吧:
[1, 2, 3]
.publisher
.map({ (val) in
return val * 3
})
.sink(
receiveCompletion: { completion in
switch completion {
case .failure(let error):
print("Something went wrong: \(error)")
case .finished:
print("Received Completion")
}
},
receiveValue: { value in
print("Received value \(value)")
}
)
清理map 表达式
我们可以通过以下方式进一步清理地图:
-
使用隐式返回
map({ (val) in
return val * 3
})
-
使用隐式返回
map({ (val) in
val * 3
})
-
删除参数声明周围不必要的括号
map({ val in
val * 3
})
-
删除不必要的换行符。有时它们对于视觉分离事物很有用,但这是一个足够简单的闭包,它只会增加不必要的噪音
map({ val in val * 3 })
-
使用隐式参数,而不是 val,这无论如何都是非描述性的
map({ $0 * 3 })
-
使用尾随闭包语法
map { $0 * 3 }
最终结果
带有编号的行,所以我可以很容易地参考。
/* 1 */[1, 2, 3]
/* 2 */ .publisher
/* 3 */ .map { $0 * 3 }
/* 4 */ .sink(
/* 5 */ receiveCompletion: { completion in
/* 6 */ switch completion {
/* 7 */ case .failure(let error):
/* 8 */ print("Something went wrong: \(error)")
/* 9 */ case .finished:
/* 10 */ print("Received Completion")
/* 11 */ }
/* 12 */ },
/* 13 */ receiveValue: { value in
/* 14 */ print("Received value \(value)")
/* 15 */ }
/* 16 */ )
通过它。
第 1 行,[1, 2, 3]
第 1 行是一个数组字面量。这是一个表达式,就像1、"hi"、true、someVariable 或 1 + 1。像这样的数组不需要分配给任何东西就可以使用。
有趣的是,这并不一定意味着它是一个数组。相反,Swift 有ExpressibleByArrayLiteralProtocol。任何符合标准的类型都可以从数组字面量中初始化。例如,Set 符合,所以你可以写:let s: Set = [1, 2, 3],你会得到一个包含1、2 和3 的Set。在没有其他类型信息的情况下(例如上面的 Set 类型注释),Swift 使用 Array 作为首选的数组字面量类型。
第 2 行,.publisher
第 2 行调用数组字面量的 publisher 属性。这将返回一个Sequence<Array<Int>, Never>。这不是常规的Swift.Sequence,它是一个非通用协议,而是在Combine 模块的Publishers 命名空间(无大小写枚举)中找到。所以它的完全限定类型是Combine.Publishers.Sequence<Array<Int>, Never>。
这是一个Publisher,其Output 为Int,其Failure 类型为Never(即不可能出现错误,因为无法创建Never 的实例类型)。
第 3 行,map
第 3 行正在调用上述 Combine.Publishers.Sequence<Array<Int>, Never> 值的 map 实例函数(又名方法)。每当一个元素通过这个链时,它都会被传递给map 的闭包所转换。
-
1 会进去,3 会出来。
- 然后
2会进去,6会出来。
- 最后
3 会进去,6 会出来。
到目前为止这个表达式的结果是另一个Combine.Publishers.Sequence<Array<Int>, Never>
第 4 行,sink(receiveCompletion:receiveValue:)
第 4 行是对 Combine.Publishers.Sequence<Array<Int>, Never>.sink(receiveCompletion:receiveValue:) 的调用。有两个闭包参数。
-
{ completion in ... } 闭包作为参数提供给标记为receiveCompletion: 的参数
-
{ value in ... } 闭包作为参数提供给标记为receiveValue: 的参数
Sink 正在为我们上面的 Subscription<Array<Int>, Never> 值创建一个新订阅者。当元素通过时,receiveValue 闭包将被调用,并作为参数传递给其value 参数。
最终发布者将完成,调用receiveCompletion: 闭包。 completion 参数的参数将是 Subscribers.Completion 类型的值,它是具有 .failure(Failure) 大小写或 .finished 大小写的枚举。由于Failure类型是Never,所以这里实际上不可能创建.failure(Never)的值。所以完成总是.finished,这将导致print("Received Completion")被调用。语句print("Something went wrong: \(error)") 是死代码,永远无法到达。
关于“声明式”的讨论
没有任何单一的句法元素可以使这段代码符合“声明性”的条件。声明式风格是与“命令式”风格的区别。在命令式风格中,您的程序由一系列命令式或要完成的步骤组成,通常具有非常严格的顺序。
在声明式风格中,您的程序由一系列声明组成。实现这些声明所需的细节被抽象出来,例如像Combine和SwiftUI这样的库。例如,在本例中,您声明每当有来自[1, 2, 3].publisher 的数字进入时,将打印该数字的三倍的print("Received value \(value)")。发布者是一个基本示例,但您可以想象一个发布者从文本字段发出值,其中事件发生在未知时间。
我最喜欢的伪装命令式和声明式风格的例子是使用像Array.map(_:)这样的函数。
你可以写:
var input: [InputType] = ...
var result = [ResultType]()
for element in input {
let transformedElement = transform(element)
result.append(result)
}
但是有很多问题:
- 您最终会在整个代码库中重复大量样板代码,只有细微的差别。
- 阅读起来比较麻烦。由于
for 是一个通用的构造,所以这里有很多事情是可能的。要确切了解发生了什么,您需要了解更多细节。
-
您错过了优化机会,因为没有致电Array.reserveCapacity(_:)。这些对append 的重复调用可以达到result 数组缓冲区的最大容量。那时:
- 必须分配一个新的更大的缓冲区
-
result 的现有元素需要复制过来
- 需要释放旧缓冲区
- 最后,必须添加新的
transformedElement
这些操作可能会很昂贵。随着您添加越来越多的元素,您可能会多次耗尽容量,从而导致多次此类重新增长操作。通过调用result.reserveCapacity(input.count),您可以告诉数组预先分配一个大小合适的缓冲区,这样就不需要重新增长操作。
result 数组必须是可变的,即使在构造完成后您可能永远不需要对其进行修改。
此代码可以改为调用map:
let result = input.map(transform)
这有很多好处:
- 它更短(虽然并不总是一件好事,在这种情况下,让它更短不会有任何损失)
- 更清楚了。
map 是一个非常特殊的工具,它只能做一件事。一看到map,就知道input.count == result.count,结果是transform函数/闭包的输出数组。
- 它已经过优化,内部
map 调用reserveCapacity,它永远不会忘记这样做。
-
result 可以是不可变的。
调用map 遵循更具声明性的编程风格。您不会摆弄数组大小、迭代、追加或其他任何细节。如果你有input.map { $0 * $0 },你说的是“我想要输入的元素平方”,结束。 map 的实现将有for 循环、appends 等。虽然它是以命令式风格实现的,但该函数将其抽象出来,并让您可以在更高的抽象级别编写代码,而您无需处理诸如 for 循环之类的不相关的事情。