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(...)的参数。
答案 0 :(得分:3)
开始时,如果格式正确,则阅读/理解此代码会容易得多。让我们开始吧:
[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, 2, 3]
第1行是数组文字。它是一个表达式,就像1
,"hi"
,true
,someVariable
或1 + 1
一样。像这样的数组不需要分配任何东西就可以使用。
有趣的是,这并不一定意味着它是一个数组。相反,Swift具有ExpressibleByArrayLiteralProtocol
。任何符合条件的类型都可以从数组文字中初始化。例如,Set
符合要求,因此您可以编写:let s: Set = [1, 2, 3]
,然后您将得到一个包含Set
,1
和2
的{{1}}。在没有其他类型信息的情况下(例如,上面的3
类型注释),Swift使用Set
作为首选的数组文字类型。
Array
第2行正在调用数组文字的.publisher
属性。这将返回publisher
。这不是常规的Swift.Sequence
,它是非通用协议,而是在Sequence<Array<Int>, Never>
模块的Publishers
名称空间(无大小写枚举)中找到的。因此它的完全限定类型为Combine.Publishers.Sequence<Array<Int>, Never>
。
这是一个Combine
,其Publisher
为Output
,其Int
类型为Failure
(即,不可能发生错误,因为没有创建Never
类型的实例的方法)。
Never
第3行正在调用上述map
值的map
实例函数(又称方法)。每当元素通过该链传递时,都会通过给定Combine.Publishers.Sequence<Array<Int>, Never>
的闭包来对其进行转换。
map
进入,1
出来。3
进入,2
出来。6
进入,3
出来。到目前为止,该表达式的结果是另一个6
Combine.Publishers.Sequence<Array<Int>, Never>
第4行是对Combine.Publishers.Sequence<Array<Int>, Never>.sink(receiveCompletion:receiveValue:)
的呼叫。有两个闭包参数。
sink(receiveCompletion:receiveValue:)
闭包作为参数为{ completion in ... }
的参数的自变量receiveCompletion:
闭包作为参数为{ value in ... }
的参数的自变量接收器正在为我们上面的receiveValue:
值创建一个新订阅者。当元素通过时,将调用Subscription<Array<Int>, Never>
闭包,并将其作为参数传递给其receiveValue
参数。
最终,发布者将完成,调用value
关闭。 receiveCompletion:
参数的参数将是类型Subscribers.Completion
的值,该值是具有completion
情况或.failure(Failure)
情况的枚举。由于.finished
类型为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
,您可以告诉数组预先分配一个大小合适的缓冲区,这样就不需要进行增长操作了。
result.reserveCapacity(input.count)
数组必须是可变的,即使您可能不需要在构造后对其进行突变。
该代码可以写为对result
的调用:
map
这有很多好处:
let result = input.map(transform)
是一种非常特殊的工具,只能做一件事。一看到map
,就会知道map
,并且结果是input.count == result.count
函数/闭包的输出的数组。transform
调用map
,并且永远不会忘记这么做。reserveCapacity
是不可变的。调用result
遵循一种更具声明性的编程风格。您无需摆弄数组大小,迭代,附加等内容。如果您有map
,则说的是“我希望输入的元素平方”。 map的实现将需要执行input.map { $0 * $0 }
循环和for
等操作。虽然它以命令式实现,但该函数将其抽象化,并允许您以更高的抽象级别编写代码,而不必在乎诸如append
循环之类的无关紧要的东西。
答案 1 :(得分:2)
首先,关于文字。您可以在任何可以使用包含相同值的变量的地方使用文字。两者之间没有重要区别
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
当您依次链接一个函数的许多 时,拆分实际上是增加可读性的一种好方法。代替
let c = [1,2,3].filter {$0>2}.count
写得更好
let c = [1,2,3]
.filter {$0>2}
.count
或什至更加清晰
let c = [1,2,3]
.filter {
$0>2
}
.count
这就是您所显示的代码中发生的所有事情:一个文字,后面是一长串方法调用。为了清晰起见,它们被分为几行。
因此,您在问题中提及的内容与Combine无关。这只是有关Swift语言的基本知识。您正在谈论的一切都可能(并且确实)发生在根本不使用Combine的代码中。
因此,从语法角度来看,除了知道每个方法调用都返回一个可以应用下一个方法调用的值(就像上面我自己的示例代码一样)之外,没有什么是“继续进行中不可见的” ,其中我将.count
应用于.filter
的结果)。当然,由于您的示例是Combine,因此某事是“不可见的”,也就是说,这些值中的每一个都是发布者,运算符或订阅者(而这些订阅者实际上是在订阅)。但这基本上只是知道什么是Combine。所以:
[1,2,3]
是一个序列数组,因此具有publisher
方法。
可以应用于序列的publisher
方法产生一个发布者。
可以将map
方法(Combine的map
,而不是Array的map
)应用于发布者,并生成另一个对象,即发布者。
sink
方法可应用于此方法,并产生一个订阅者,这就是链的末端。