swift 组合声明语法
swift combine declarative syntax
SwiftCombine 的声明语法对我来说看起来很奇怪,而且似乎有很多不可见的事情正在发生。
例如,以下代码示例在 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] 创建的数组文字实例。我想它是一个数组文字,但我不习惯看到它 "declared" 而不是将它分配给变量名或常量或使用 _=.
我特意在 .publisher 之后添加了新行。 Xcode 是否忽略空格和换行符?
由于这种风格,或者我对这种风格的视觉解析很新,我错误地认为“,receiveValue:”是一个可变参数或一些新语法,但后来意识到它实际上是 .sink(.. .).
文字
首先,关于文字。您可以在任何可以使用包含相同值的变量的地方使用文字。
之间没有重要区别
let arr = [1,2,3]
let c = arr.count
和
let c = [1,2,3].count
空格
其次,关于空格。简而言之,Swift 不关心您是否在点之前拆分语句。所以
没有区别
let c = [1,2,3].count
和
let c = [1,2,3]
.count
链接
当您一个接一个地链接 lot 函数时,拆分实际上是提高易读性的好方法。而不是
let c = [1,2,3].filter {[=14=]>2}.count
写的更好看
let c = [1,2,3]
.filter {[=15=]>2}
.count
或者为了更清楚
let c = [1,2,3]
.filter {
[=16=]>2
}
.count
结论
这就是您显示的代码中发生的所有事情:文字后跟一长串方法调用。为了便于阅读,它们被分成不同的行,仅此而已。
所以你在问题中提到的任何内容都与 Combine 无关。这只是关于 Swift 语言的基本内容。您所谈论的一切都可能(并且确实)发生在根本不使用 Combine 的代码中。
所以从语法的角度来看,没有什么是"going on that is not visible",除了知道每个方法调用returns一个值,下一个方法调用可以应用(就像我自己的上面的示例代码,其中我将 .count
应用于 .filter
的结果)。当然,由于您的示例是 Combine,因此 something 是 "going on that is not visible",即这些值中的每一个都是发布者、运营商或订阅者(并且订阅者确实订阅了).但这基本上只是了解什么是 Combine 的问题。所以:
[1,2,3]
是一个数组,它是一个序列,所以它有一个publisher
方法。
publisher
方法,可应用于序列,产生发布者。
map
方法(Combine的map
,不是Array的map
)可以应用于一个publisher,产生另一个对象,它是一个publisher。
sink
方法可以应用于它,并产生一个订阅者,这就是链的末端。
先清理代码
正在格式化
首先,reading/understanding如果格式正确,这段代码会容易得多。那么让我们从这里开始:
[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
表达式
我们可以通过以下方式进一步清理地图:
使用隐式 return
map({ (val) in
return val * 3
})
使用隐式 return
map({ (val) in
val * 3
})
删除参数声明周围不必要的括号
map({ val in
val * 3
})
删除不必要的换行符。有时它们对于在视觉上分离事物很有用,但这是一个足够简单的闭包,它只是添加了不需要的噪音
map({ val in val * 3 })
使用隐式参数,而不是 val
,无论如何它都是非描述性的
map({ [=15=] * 3 })
使用尾随闭包语法
map { [=16=] * 3 }
最终结果
有编号的行,所以我可以很容易地参考。
/* 1 */[1, 2, 3]
/* 2 */ .publisher
/* 3 */ .map { [=17=] * 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
属性。这 return 是 Sequence<Array<Int>, Never>
。这不是常规 Swift.Sequence
, which is a non-generic protocol, but rather, it's found in the Publishers
namespace (a case-less enum) of the Combine
module. So its fully qualified type is Combine.Publishers.Sequence<Array<Int>, Never>
.
这是一个 Publisher
,其 Output
为 Int
,其 Failure
类型为 Never
(即不可能出现错误,因为有无法创建 Never
类型的实例)。
第 3 行,map
第 3 行正在调用上面 Combine.Publishers.Sequence<Array<Int>, Never>
值的 map
实例函数(a.k.a. 方法)。每次一个元素通过这个链时,它都会被给定的闭包转换 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)")
语句是死代码,永远达不到。
关于"declarative"的讨论
没有单一的句法元素使此代码符合 "declarative" 的条件。声明式风格与 "imperative" 风格不同。在命令式风格中,您的程序由一系列命令或要完成的步骤组成,通常具有非常严格的顺序。
在声明式风格中,您的程序由一系列声明组成。实现这些声明所必需的细节被抽象出来,例如 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
function/closure. 的输出数组
- 已优化,内部
map
调用reserveCapacity
,永远不会忘记这样做。
result
可以是不可变的。
调用 map
遵循更具声明性的编程风格。您不会摆弄数组大小、迭代、追加或其他任何细节。如果你有 input.map { [=103=] * [=103=] }
,你就是说 "I want the input's elements squared",结束。 map 的实现需要 for
循环、append
s 等。虽然它是以命令式风格实现的,但该函数将其抽象化,并允许您在更高的抽象级别上编写代码,而不必处理 for
循环等不相关的事情。
SwiftCombine 的声明语法对我来说看起来很奇怪,而且似乎有很多不可见的事情正在发生。
例如,以下代码示例在 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] 创建的数组文字实例。我想它是一个数组文字,但我不习惯看到它 "declared" 而不是将它分配给变量名或常量或使用 _=.
我特意在 .publisher 之后添加了新行。 Xcode 是否忽略空格和换行符?
由于这种风格,或者我对这种风格的视觉解析很新,我错误地认为“,receiveValue:”是一个可变参数或一些新语法,但后来意识到它实际上是 .sink(.. .).
文字
首先,关于文字。您可以在任何可以使用包含相同值的变量的地方使用文字。
之间没有重要区别let arr = [1,2,3]
let c = arr.count
和
let c = [1,2,3].count
空格
其次,关于空格。简而言之,Swift 不关心您是否在点之前拆分语句。所以
没有区别let c = [1,2,3].count
和
let c = [1,2,3]
.count
链接
当您一个接一个地链接 lot 函数时,拆分实际上是提高易读性的好方法。而不是
let c = [1,2,3].filter {[=14=]>2}.count
写的更好看
let c = [1,2,3]
.filter {[=15=]>2}
.count
或者为了更清楚
let c = [1,2,3]
.filter {
[=16=]>2
}
.count
结论
这就是您显示的代码中发生的所有事情:文字后跟一长串方法调用。为了便于阅读,它们被分成不同的行,仅此而已。
所以你在问题中提到的任何内容都与 Combine 无关。这只是关于 Swift 语言的基本内容。您所谈论的一切都可能(并且确实)发生在根本不使用 Combine 的代码中。
所以从语法的角度来看,没有什么是"going on that is not visible",除了知道每个方法调用returns一个值,下一个方法调用可以应用(就像我自己的上面的示例代码,其中我将 .count
应用于 .filter
的结果)。当然,由于您的示例是 Combine,因此 something 是 "going on that is not visible",即这些值中的每一个都是发布者、运营商或订阅者(并且订阅者确实订阅了).但这基本上只是了解什么是 Combine 的问题。所以:
[1,2,3]
是一个数组,它是一个序列,所以它有一个publisher
方法。publisher
方法,可应用于序列,产生发布者。map
方法(Combine的map
,不是Array的map
)可以应用于一个publisher,产生另一个对象,它是一个publisher。sink
方法可以应用于它,并产生一个订阅者,这就是链的末端。
先清理代码
正在格式化
首先,reading/understanding如果格式正确,这段代码会容易得多。那么让我们从这里开始:
[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
表达式
我们可以通过以下方式进一步清理地图:
使用隐式 return
map({ (val) in return val * 3 })
使用隐式 return
map({ (val) in val * 3 })
删除参数声明周围不必要的括号
map({ val in val * 3 })
删除不必要的换行符。有时它们对于在视觉上分离事物很有用,但这是一个足够简单的闭包,它只是添加了不需要的噪音
map({ val in val * 3 })
使用隐式参数,而不是
val
,无论如何它都是非描述性的map({ [=15=] * 3 })
使用尾随闭包语法
map { [=16=] * 3 }
最终结果
有编号的行,所以我可以很容易地参考。
/* 1 */[1, 2, 3]
/* 2 */ .publisher
/* 3 */ .map { [=17=] * 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
属性。这 return 是 Sequence<Array<Int>, Never>
。这不是常规 Swift.Sequence
, which is a non-generic protocol, but rather, it's found in the Publishers
namespace (a case-less enum) of the Combine
module. So its fully qualified type is Combine.Publishers.Sequence<Array<Int>, Never>
.
这是一个 Publisher
,其 Output
为 Int
,其 Failure
类型为 Never
(即不可能出现错误,因为有无法创建 Never
类型的实例)。
第 3 行,map
第 3 行正在调用上面 Combine.Publishers.Sequence<Array<Int>, Never>
值的 map
实例函数(a.k.a. 方法)。每次一个元素通过这个链时,它都会被给定的闭包转换 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)")
语句是死代码,永远达不到。
关于"declarative"的讨论
没有单一的句法元素使此代码符合 "declarative" 的条件。声明式风格与 "imperative" 风格不同。在命令式风格中,您的程序由一系列命令或要完成的步骤组成,通常具有非常严格的顺序。
在声明式风格中,您的程序由一系列声明组成。实现这些声明所必需的细节被抽象出来,例如 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
function/closure. 的输出数组
- 已优化,内部
map
调用reserveCapacity
,永远不会忘记这样做。 result
可以是不可变的。
调用 map
遵循更具声明性的编程风格。您不会摆弄数组大小、迭代、追加或其他任何细节。如果你有 input.map { [=103=] * [=103=] }
,你就是说 "I want the input's elements squared",结束。 map 的实现需要 for
循环、append
s 等。虽然它是以命令式风格实现的,但该函数将其抽象化,并允许您在更高的抽象级别上编写代码,而不必处理 for
循环等不相关的事情。