swift - 快速组合声明式语法
问题描述
Swift Combine 的声明式语法对我来说看起来很奇怪,而且似乎有很多事情是不可见的。
例如,以下代码示例在 Xcode 游乐场中构建和运行:
[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)")
})
我看到我假设是用 [1, 2, 3] 创建的数组文字实例。我猜它是一个数组文字,但我不习惯看到它“声明”而不将它分配给变量名或常量或使用_=。
我故意在 .publisher 之后添加了一个新行。Xcode 是否忽略了空格和换行符?
由于这种风格,或者我对视觉解析这种风格的新奇,我错误地认为“,receiveValue:”是一个可变参数或一些新语法,但后来意识到它实际上是.sink(...)的一个参数。
解决方案
先清理代码
格式化
首先,如果格式正确,阅读/理解这段代码会容易得多。所以让我们从那个开始:
[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 })
使用隐式参数,而不是 a
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
or一样1 + 1
。像这样的数组不需要分配给任何东西就可以使用。
有趣的是,这并不一定意味着它是一个数组。相反,Swift 拥有ExpressibleByArrayLiteralProtocol
. 任何符合标准的类型都可以从数组字面量中初始化。例如,Set
符合,所以你可以写:let s: Set = [1, 2, 3]
,你会得到一个Set
包含1
,2
和3
。在没有其他类型信息的情况下(例如Set
上面的类型注释),Swift 将Array
其用作首选的数组字面量类型。
2号线,.publisher
第 2 行正在调用publisher
数组文字的属性。这会返回一个Sequence<Array<Int>, Never>
. 这不是常规的Swift.Sequence
,它是非通用协议,而是在模块的Publishers
命名空间(无大小写枚举)中找到Combine
。所以它的完全限定类型是Combine.Publishers.Sequence<Array<Int>, Never>
.
它是whois ,其Publisher
类型是(即不可能出现错误,因为无法创建该类型的实例)。Output
Int
Failure
Never
Never
3号线,map
第 3 行正在调用上述值的map
实例函数(又名方法)Combine.Publishers.Sequence<Array<Int>, Never>
。每当一个元素通过这个链时,它都会被给定的闭包所转换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:
Subscription<Array<Int>, Never>
Sink 正在为我们上面的价值创建一个新的订阅者。当元素通过时,receiveValue
闭包将被调用,并作为参数传递给它的value
参数。
最终发布者将完成,调用receiveCompletion:
闭包。param的completion
参数将是 type 的值Subscribers.Completion
,它是一个带有.failure(Failure)
case 或.finished
case 的枚举。由于Failure
类型是Never
,实际上不可能在.failure(Never)
这里创建值。所以完成将永远是.finished
,这将导致print("Received Completion")
被调用。该语句print("Something went wrong: \(error)")
是死代码,永远无法到达。
关于“声明式”的讨论
没有单一的句法元素可以使这段代码符合“声明性”的条件。声明式风格是与“命令式”风格的区别。在命令式风格中,您的程序由一系列命令式或要完成的步骤组成,通常具有非常严格的顺序。
在声明式风格中,您的程序由一系列声明组成。实现这些声明所必需的细节被抽象出来,例如像Combine
和SwiftUI
. 例如,在这种情况下,您声明print("Received value \(value)")
该数字是三倍的,只要有一个数字来自[1, 2, 3].publisher
. 发布者是一个基本示例,但您可以想象一个发布者从文本字段发出值,其中事件发生在未知时间。
我最喜欢伪装命令式和声明式风格的例子是使用类似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
必须添加新的
这些操作可能会变得昂贵。随着您添加越来越多的元素,您可能会多次耗尽容量,从而导致多次此类重新增长操作。通过 callined
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
循环、append
s 等。for
虽然它是以命令式风格实现的,但该函数将其抽象化,并允许您在更高的抽象级别编写代码,而您不会在循环等不相关的事情上胡思乱想。
推荐阅读
- python - 轴标签间距不正确
- java - DDD 中域对象集合的最佳实践
- javascript - Antenna House 6.6 是否支持 HTML DOM classList 切换?
- reactjs - React (TS) 中的 Enzyme / Jest Error 测试组件渲染
- python - 清除多个标签值
- reactjs - React Native 组件在动画之前短暂渲染
- excel - 如何跨行复制 Excel 公式,以便单元格引用行增量和单元格引用列保持固定
- r - 使用 mutate 函数并制作条件语句,r
- list - 从python中的文件中获取随机名称
- hbase - Hbase 快照还原失败:TableExistsException