并发
并发是在Swift编程语言中处理并发和封装异步工作的小型工具包。处理并行的范式有很多种,我认为它们各自都有特定的用途。最值得注意的是,对于持续更新、订阅和数据处理流水线来说,RxSwift几乎是王者。并发旨在填补其他用途的空白,我认为,在早期,我为为何永远不要再使用旧的(ExpectedType?, Error?)->()
样式完成处理器的风格提供了相当有力的论据。
致谢
尽管我没有参考它们的源代码,但我对Promise/Future的实现受到了我在专业和个人领域广泛使用Deferred和BrightFutures的启发。
目录
要解决的问题的第一组
坦白说:Swift中的完成处理程序实属头疼。简单来说,让我们分析一下Swift标准回调模式的繁琐之处:
func getAString(_ completionHandler: (String?, Error?)->())
首先,这要求函数的消费者需要解包两个不同的值来了解实际的状况,最终导致用户不得不实现一段冗长的switch
或if
/else if
/else
代码块来处理。
getAString { (string, error) in
if let string = string {
self.doSuccessThingy(string)
}
else if let error = error {
self.doErrorThingy(error)
}
}
用户需要手动编写这些逻辑,无法获得自动完成提示,也无法从编译器那里获得关于遗漏逻辑情况的提示。
这也揭示了这种模式的另一个主要问题:你注意到没有else
情况吗?尽管它涵盖了两个逻辑上预期的结果状态,但这仍然是一个不完整的逻辑语句。可怕的是,回调签名的本质允许两个参数都为nil的可能性,这对消费者来说是完全不合逻辑的状态。但是,由于这个可能性,他们被迫处理它。
对于Swift开发人员来说,解包可选性已经是不得不频繁做的事情,没有人喜欢模糊的、可能不连贯的状态。那么,解决方案是什么呢?让左边登场:我的好朋友Result。
角色阵容
Result
Swift 5中添加了Result<Success, Failure>
,并且人们欢呼雀跃。"人们"在这里指的是所有人,包括讨厌(Success?, Error?)->
完成块的每个人,阅读我上面出色的论点后,现在也包括你。在并行处理的2.0版之前,它包括一个Result类型,但现在它在Swift的本机Result上还包括了一堆扩展方法和属性。
这些扩展为你带来了什么?为什么,当然是因为“甜蜜、甜蜜的语法糖”!我给Result添加了一些类似于函数式的实用方法,这对于使用其他Promise/Future库或像RxSwift这样的响应式框架的人来说将非常熟悉。我包括onSuccess(_:)
和onError(_:)
方法,允许消费者独立实现成功块和错误块。这意味着消费者可以单独实现一个、两个、两者或都不实现。并且,由于这些方法每个都返回一个可丢弃的引用到Result,消费者可以轻松地按顺序链接它们,如下面所示。
两个
getAString { (result) in
result.onSuccess { (value) in
self.doSuccessThingy(value)
}.onError { (error) in
self.doErrorThingy(error)
}
}
或者只一个
getAString { (result) in
result.onSuccess { (value) in
self.doSuccessThingy(value)
}
}
因此,当我们必须使用完成块时,这些对Result
的扩展提供了整洁的、编译器辅助的、无需展开的选项。
BLOCKCEPTION!
完成块的缺点:通常,异步工作依赖于其他异步工作。在一个传统完成块的世界里,这意味着至少要写一个有完成处理器的函数,然后这个函数又调用另一个有完成处理的函数。并且千方百计让你的异步方法需要连续调用多个完成块。那么,即使你只实现成功块(天哪!)你也可能遇到这样的噩梦
func getAStringWithTwoNumbersInIt(_ completionHandler:(Result<String, MyError>)->()) {
getInt1 { result1 in
result1.onSuccess { int1 in
getInt2 { result2 in
result1.onSuccess { int2 in
completionHandler(.success("I got \(int1) and \(int2)! Yay!"))
}
}
}
}
}
末日金字塔:它不仅仅是为了可选绑定!
那么,我们如何解决这个问题呢?
Promise 和 Future
如果您从未接触过Promise 和/或 Future的概念,我真的非常高兴能向您介绍它们!
概念
这个概念相当简单:未来(在某些范例中称为“延迟”或“最终”)是一个作为未来结果的占位符的对象。所以如果你编写一个使用未来而不是完成块的异步方法,那么未来将以同步的方式返回。未来代表了返回它的方法正在执行的异步工作时的一部分,并负责处理其完成。
所以,如果你有一个看起来像下面的方法,包含一个完成块
func askForHerPhoneNumber(_ completionHandler:(Int?, NSError?)->())
那么,使用未来,它会这样
func askForHerPhoneNumber() -> Future<Int, NSError>
提供未来的信息
这里就出现了 Promise<T,E>
的用武之地。Promise几乎只有一个目的:销售一个Future<T,E>
,并负责解决或拒绝它。仅此而已。
所以,如果你正在编写一个仅仅为了封装另一个使用传统完成块的方法而返回承诺的方法,那么它可能看起来像这样
func goCallYourMother() -> Future<AnEarful, CallError> {
let promise = Promise<AnEarful, CallError>()
callYourMotherWithCompletion { (earful, error) in
if let earful = earful {
promise.resolve(earful)
}
else if let error = error {
promise.reject(error)
}
else {
let noDataError = CallError(message: "No data returned by the call.")
promise.reject(noDataError)
}
}
return promise.future
}
这里的关键特性是
- 同步地,创建一个承诺,然后返回承诺的未来。
- 在异步工作完成后,你可以在承诺上调用
resolve(_:)
方法并传入成功值,或者传入类型为E
的错误的reject(_:)
方法。你在创建承诺时确定期望的错误类型。
消费未来
这将是孩子们玩得开心的地方!
未来有两种 typealias
块类型,SuccessBlock
((T)->()
)和 ErrorBlock
((E)->()
),并有两个公共方法,它们将这些块作为参数。这两个方法是你的日常饮食
onSuccess(_:)
和 onError(_:)
调用这些方法以给出未来的完成行为。如果调用在设置这些之前发生,传入的块将简单地随即将执行。简单易懂。
因此,消费未来是这样做的
let future: Future<AnEarful, PhoneCallError> = goCallYourMother()
future.onSuccess { (earful) in
self.hooBoy(earful)
}.onError { (error) in
self.wellAtLeastITried(error)
}
或者,如果你想更加简洁,由于所有这些方法都返回 @discardableResult
引用,你根本不需要将未来分配给任何变量
goCallYourMother().onSuccess { (earful) in
self.hooBoyWhatAn(earful)
}.onError { (error) in
self.wellAtLeastITried(error)
}
注意:还有一个便利的 finally(_:)
方法,它可以在完成后将一个块添加到执行,无论成功或失败。它在给定的成功或失败块之后执行,并且将 Result<T,E>
作为其一个参数传递。
未来真正神奇的地方在于 onSuccess(_:)
和 onError(_:)
可以根据需要调用多次,并且每个操作将以顺序执行。所以,如果你有一个检索值并返回承诺的方法,并且应用的不同层都需要更新该值,你可以从方法到方法传递这个未来,同时在你前往的过程中追加成功动作。
是的,我知道,需要举一个例子。那么,假设我们有一个之前提到的相同的方法:func goCallYourMother() -> Future<AnEarful, PhoneCallError>
。我们可以这样传播它:
func callYourMomAndThenReflect() -> Future<AnEarful, PhoneCallError> {
return goCallYourMother().onSuccess { (earful) in
self.hooBoyWhatAn(earful) // a local action that needs to happen
}.onError { (error) in
self.wellAtLeastITried(error) //also a local action that needs to happen
}
}
因此,我们已经调用了一个获取未来的方法,附加了一个 SuccessBlock
和一个 ErrorBlock
,然后立即将这个未来返回给使用这个方法的人。下一个消费者可以做同样的事情,而且一直这样下去,一旦最初的调用完成,就会依次传递完成的报告或错误,直到最后一个块。
但是,你会问,如果你在应用程序的不同层次需要的值类型不同怎么办呢?
不用说了,我已经明白了,老铁。
变异
Future 包含了一系列多功能的变异方法,那些熟悉 Rx 的人会觉得很熟悉。这些方法允许映射值(T
)、错误(E
)和这两者的组合。其中第一个,执行完全映射:
映射结果
@discardableResult func mapResult<NewT, NewE: Error>(_ mapBlock:@escaping (Result<T, E>) -> (Result<NewT, NewE>)) -> Future<NewT, NewE>
这将生成一个新的 Future,具有可能不同的成功和/或错误类型。当你调用它时,传入一个映射块。这个块在第一个 Future 完成(翻译为 Result<T,E>
)后会被调用,从与你的 Future 相匹配的 T
和 E
类型转换为与你的新的 Future 上类型匹配的 Result<NewT, NewE>
。这将自动生成一个新的 Future<NewT, NewE>
,当第一个 Future 完成(翻译为自动完成时),它会相应的完成。
这样可以让你用一种方法映射两种类型,如下所示
let myIntFuture: <Int, MyIntError> = getThatInt()
let myStringFuture = myIntFuture.mapResult { (result) -> Result<String, MyStringError>
switch result {
case .success(let firstValue):
if firstValue < 5 {
return .success("\(firstValue)")
}
else {
return .failure(.couldntMakeString)
}
case .failure(let firstError):
return .failure(.couldntGetInt)
}
}
// myStringFuture will be of type Future<String, MyStringError>
映射值
现在,想象一下,我们有一个获取电话号码的未来,但是以整数形式获取它。然后想象一下,我们想要获取它,但是以字符串形式。因此,我们想要映射值,但不需要改变错误类型。
@discardableResult func mapValue<NewValue>(_ mapBlock:@escaping (T) -> (NewValue)) -> Future<NewValue, E>
该方法生成了一个新的未来,它具有相同的错误类型,但具有新的值类型。因此,假设我们有一个第一个方法:getPhoneNumberInt() -> Future<Int, NSError>
,我们可以轻松地按照如下方式映射它:
func getPhoneNumberString() -> Future<String, NSError> {
return getPhoneNumberInt().mapValue { "\($0)" }
}
地图错误
有时我们希望直接传递数据值,但又想将其抽象为更专业的错误。例如,你可能有特定于网络层的错误,并希望保持这一层良好的封装。对于这种情况,我们有
@discardableResult func mapError<NewError: Error>(_ mapBlock:@escaping (E) -> (NewError)) -> Future<T, NewError>
所以,如果我们有一个方法,如 getTweetfacePostFromNetwork(id: String) -> Future<TweetfacePost, NetworkError>
,我们可以按照需要简单地映射到新的错误类型
func getTweetfacePost(id: String) -> Future<TweetfacePost, TweetfaceError> {
return getTweetfacePostFromNetwork(id: id).mapError { (networkError) -> TweetfaceError in
switch networkError {
case .userError:
return TweetfaceError.badID
case .serverError:
return TweetfaceError.serverError
}
}
}
或使用便利的 mapToNSError() -> Future<T, NSError>
,它使用了 mapError(_:)
以及 Foundation 的无侵入性的从 Error
到 NSError
的桥接
func getTweetfacePost(id: String) -> Future<TweetfacePost, NSError> {
return getTweetfacePostFromNetwork(id: id).mapToNSError()
}
Flatmap
如果您不想自己处理错误映射,或者需要可失败映射,或者想映射并获取有关是否为原始错误或映射失败更详细的信息,那么 flatMap(_:)
就很合适!
@discardableResult func flatMap<Q>(_ mapBlock:@escaping (T) -> (Q?)) -> Future<Q, MapError<T,E,Q>>
在许多情况下,第一个 future 的成功结果可能与映射 future 的成功结果不匹配,这时就使用了 flatMap(_:)
。它允许第一个 future 的成功结果可能触发第二个 future 的失败,如果传入的 block 返回 nil。
现在,您已经可以使用 mapResult(_:)
来实现这个功能,它看起来就像这样
let getTheNumber: Future<Int, IntError> = getInteger()
let writeItDown = getTheNumber.mapResult { (result) -> Result<String, StringError>
switch result {
case .success(let intValue):
if intValue < 5 {
return .success("\(intValue)")
}
else {
return .failure(StringError.couldntMapInt)
}
case .failure(let error):
return .failure(StringError.couldntGetInt)
}
}
然而,这迫使我们选择一种错误类型,并必须为您的新错误类型生成一个与错误映射相对应的错误情况(如上所示为 .couldntMapInt
)。但是,使用 flapMap,它就很简单了
let getTheNumber: Future<Int, IntError> = getInteger()
let writeItDown = getTheNumber.flatMap { (intValue) -> String?
return (intValue < 5) ? "\(intValue)" : nil
}
那么,你可能会问,这个新 future 的错误类型是什么呢?好一个问题,敏锐的读者!答案是这个方便的小家伙
MapError<T, E, Q>
MappError 表示 map_flatClickable (可以在 Future 或 Result 上调用) 的两种可能的失败状态。正如您在这里所看到的,MapError 有一个用于第一个 future 失败的情况,还有另一个用于映射失败的案例。
enum MapError<SourceType, SourceError, TargetType>: Error {
case originalError(SourceError)
case mappingError(SourceType)
}
因此,您可以从一个统一的错误中获取所有需要关于失败的数据,并且它是自动生成的,所以您不必手动创建。我知道,我知道。感谢您的光临。与 .mappingError
案例相关联的值将是导致 nil
的 block 中传递的值。而且将提供有用的调试描述
无法将值:(6) 映射到输出类型:String。
链式调用
作为开发者,我们经常会遇到多个相关联的异步任务,需要将它们分组或相互连接。并发给我们的工具也有很多。
接着
@discardableResult func then<NewValue, NewError: Error>(_ mapBlock:@escaping (T)->(Future<NewValue, NewError>)) -> Future<NewValue, NewError>
有时,你必须按顺序执行一些异步任务,因为一个任务的结果可能是另一个任务开始的前提。你可以用then(_:)
方法来方便地做到这一点。例如,你有一个获取电话号码的方法func getPhoneNumber() -> Future<Int, NSError>
,还有一个 dial 电话号码的方法func call(phoneNumber: Int) -> Future<PhoneCall, PhoneError>
。很明显,在你没有得到前一个方法的返回结果之前,你不能调用后一个方法。最适合处理这种情况的方法是then(_:)
let makePhoneCall = getPhoneNumber().then { (number) -> Future<PhoneCall, PhoneError>
return call(phoneNumber: number)
}
// Or, with the beautiful interchangeability of blocks and functions in Swift, you could write it like so:
let makePhoneCall = getPhoneNumber().then(call(phoneNumber:))
归并
可能你需要完成任务,但这些任务不必按顺序执行。你只需要在继续之前完成所有任务。那么,zip(_:)
就是你的工具!
static func zip(_ futures: [Future<T, E>]) -> Future<[T], E>
zip
接收一个返回类型为 <T, E>
的未来对象的数组,并返回一个类型为 <[T], E>
的单个未来对象。这个未来对象将当选定的数组中的所有未来对象都成功时解析,或者在数组中的任何未来对象失败时被拒绝。
例如,假设你想要获取几个电话号码,但你必须要在继续之前获取所有这些电话号码(一个完全现实世界的例子,是吗?)。所以,你有一些返回 Future<Int, NumberError>
的方法,你想要在他们全部返回之后再执行一个操作。你可以这样做
let phoneNumberFutures: [Future<Int, NumberError>] = [getMomsNumber(), getDadsNumber(), getWackyPhoneNumber()]
let getPhoneNumbers = Future.zip(phoneNumberFutures)
// getPhoneNumbers will be of type Future<[Int], NumberError>
getPhoneNumbers.then { (phoneNumbers) in
commenceZanyParentTrap(with: phoneNumbers)
}
拜托了!
先完成
另一方面,你可能有成堆相似的正在进行的任务,你只需要从最先完成的任务中获取数据。有专门的方法可以做到这一点!
static func firstFinished(from futures: [Future]) -> Future
无论哪个未来(如果有的话)首先成功,都将触发联合未来的成功,如果没有任何一个成功,最后失败的将触发错误状态,并通过其错误。
最后一点...
嗯,两点最后。在我离开之前,我想指出Future上的这两个便利方法。它们是
static func preResolved(value: T) -> Future<T, E>
和
static func preRejected(error: E) -> Future<T, E>
这些简单地生成在创建时就已经完成的未来。这些方法主要有两个用途
- 测试。这可以将测试中设置的多行代码简化为一行。
- 在某方法返回未来时,如果传递的参数(或其他状态方面)不足以完成异步操作,那么这将为您提供一个选项,立即拒绝未来,并且不会将任何不必要的异步性添加到应用程序中。
结论
这就是全部!希望这些对您有所帮助,或者至少能提供信息。
我总是调整代码,寻找优化方法,并思考新功能。随时提出问题、建议,或者(如果你非常棒)提出您自己的pull requests!
所有文件都已经过测试。欢迎查看测试代码这里。
当然,请注意版本更新!
安装
Concurrency可以通过CocoaPods获取。要安装,只需将以下行添加到您的Podfile中
pod "Concurrency"
作者
Jake Hawken,www.github.com/jakehawken
许可证
Concurrency在MIT许可证下可用。更多信息请参阅LICENSE文件。