Swift并行编程:承诺

并发在我们的日常工作中越来越重要。 在我的上一篇文章(使用Swift进行并行编程:基础知识,操作)中,我们研究了Apple提供的工具。 这次,让我们看一下没有得到官方支持的东西。

总结一下:

并发是指同时进行工作的能力。

想象一下,任务长期存在。 需要一段时间,最后,我们得到了结果。 例如,这可能是下载文件。 结果将是图像。 同时,我们知道将图像设置为UIImageView。 那么我们如何实现呢?

最简单的版本是通过NSURLSession下载文件,然后将其关闭。

我们都知道,该怎么做。 所以这里没问题。 但是让我们稍微复杂一点。 我们没有该文件的链接,而是必须从后端请求该文件。 这样做将包含请求链接,将响应解析为我们期望的格式,请求图像然后进行设置。

对于像这样的简单用例,这可能会变得越来越复杂。 最后,我们处于回调地狱。

相反,我们可以创建一个委托并对此做出反应。 如果您有两个以上的请求,将很难维护。

调查操作,我们将这些任务拆分并以这种方式编写。

但是正如您所看到的,很难遵循用例的逻辑。 那么还有哪些其他选择呢?

承诺

有一个想法,考虑在将来创建包含值的变量。 它现在不需要立即包含其值,但是在某个时候,它将具有该值,然后我们就可以在其上执行代码。

这无非就是承诺-在某个时间点交付价值的保证。 对于Promise,您可以编写有关此想法的代码,即该变量有时会包含值或错误。 只要兑现了承诺,它就会被执行。

创作与创作

一个Promise包含一个带有2个回调的闭包。 您可以通过调用具有相应值的complement()来实现Promise,也可以通过错误拒绝它。

要创建Promise,只需包装要创建值的代码即可。

用法

承诺分为多个部分:承诺本身,成功代码(如果包含值)和错误代码。

只要没有代码,promise将不会执行任何代码,这将对其值做出响应。 要添加此代码,我们使用`.then()`。 这仍然不能决定执行时间。 它只是说,诺言如果愿意就可以开始。

由于经常需要创建代码链,因此我们不仅可以返回值,而且还可以返回承诺。

如果响应者链中有任何错误,执行流程将跳转到catch子句,而不继续执行其他then()闭包。

PromiseKit

有多个承诺库。 Google刚刚发布了自己的版本。 还有一个鲜为人知的竞争者,称为HoneyBee。 我将使用PromiseKit,因为它已经相当成熟并且已经开发了多年。 根据我的经验,他们的问题响应时间非常快,仅几个小时之内。 此外,PromiseKit提供了许多扩展,您可以在iOS中使用这些扩展来简化其使用过程。 由于Swift及其对Promises的解释,它确实包含一些特殊情况,但无论如何,让我们深入研究一下如何使用它。

安装

PromiseKit支持几乎所有的安装方式。 您可以通过Cocoapods做到这一点:

迦太基:

SwiftPM:

或手动执行。 这是你的选择! 这样做之后,我们就可以开始了。

创造承诺

如前所述,promise由带有两个不同回调的闭包组成。 一种回调是实现,一种是拒绝诺言。 在PromisKit中,这有点不同。 我们有一个密封对象,它有多种方法,包括complement(结果)和reject(错误)。 它还包含将自动确定promise状态是什么的resolve(结果,对象)。

从后端请求图像如下所示:

如您所见,promise总是可以选择失败。 但是我们确实有没有错误的情况(例如,返回静态文本)。 为此,PromiseKit提供了一种特殊的Promise。 这就是保证:

承诺链

创建承诺后,我们现在可以激活它。 这是通过使用then()完成的。

then()始终具有与收到的承诺不同的返回值。 如果它的主体只有一行,Swift会尝试进行类型推断。 可悲的是,这在大多数情况下都行不通,并且我们收到了一个非常具体的错误消息:

无法使用类型为’(()-> _)的参数列表调用’then’

可以通过指定闭包参数和返回类型来解决此问题。
您可以使用done()完成链。 这是您成功链条的典型结局,您无法兑现承诺。

通常的诺言链的最后一部分(除了使用保证)是catch()。 这用于在执行过程中对任何类型的错误作出反应。 如果您的链中的任何承诺都将被拒绝,则该链将被终止,执行路径将跳转至catch闭包。 在此关闭中,您可以添加错误处理,例如向用户显示消息。

与往常一样,此类情况也有例外。 有时,您不希望错误级联并具有例如默认值。 这可以通过restore()完成。

我们从后端获取一个imageurl然后该图像本身的先前示例如下所示:

其他链元素

现在,我们已经研究了承诺链的基本元素,让我们将它们提升到一个新的水平。 我们可以使用firsty()来使用一些语法糖来启动链。

如果我们希望某些代码始终在promise的末尾执行,我们将使用确保:

回顾我们的示例,我们可以添加网络指示器:

有时我们想返回我们已经收到的同样的承诺。 为此,我们可以使用get()。

如果我们需要在同一结果上执行多个命令,则这特别有用。

等待多个异步命令执行很慢或很困难。 我们同步执行它们的速度很慢,如果尝试处理回调的所有不同选项,则很难。 在PromiseKit中有when()。 在when()中,添加要同时执行的所有promise,它将等待所有promise解析后再继续。

我们一直在使用done()而不是在最后的承诺中使用。 它基本上告诉了诺言,链到此结束,并且不存在任何返回值,而then()始终具有返回值。

许诺链还有更多元素,例如:

– map(),要求您返回一个对象或值类型

– compactMap,要求您返回一个可选的。 Nil是一个错误

穿线

与往常一样,当异步工作时,我们必须考虑线程化。 这是如何与Promise一起工作的:所有Promise在后台线程上执行,但是链本身(then(),catch(),map()等)在Main线程上执行。 要知道这一点很重要,因为它可能导致某种意外行为。 想象将承诺的响应解析到您的持久层中。 如果响应很小,则在Main线程上执行该操作没有问题,但不能保证是这样。 较大的响应可能会导致丢帧甚至屏幕冻结。 为了解决这个问题,您可以将您的解析编写为Promise,也可以手动更改then()闭包的线程。

但是,建议只写promise并在其中处理线程,因为它不太容易出错。

特殊图案

为了使您更轻松地转换为Promise,让我们看一下开发人员习惯的一些模式:

延迟

有时我们遇到这种情况,希望将执行延迟特定时间。 这可以通过使用after()来实现。

暂停

由于我们知道after(),因此我们还可以创建一个超时。 当然,有一些选项可以为不同的用例设置超时(例如,NSURLSession中的网络请求),但是有时我们希望将其更改为较小的值或向Promise添加超时,默认情况下不会超时。 这可以通过使用race()来完成

请注意,竞争中的两个承诺都需要返回相同的值。 一种简单的方法是使用.asVoid()。

重试

与网络请求一样,连接断开可能会发生,我们想重试该请求。 这可以通过使用restore()来完成。

代表

UIKit的主要模式之一是委托模式。 我们可以将其包装在Promise中,但是请注意,如果您这样做,则可能无法满足您的需求。 承诺只能解决一次,因此尝试包装UIButton会导致问题。 如果仅需要一个响应,则可以通过存储密封对象并将其解析为委托来实现。

需要注意的一件事是,在存储密封对象时,任何时候都只能有一个这样的请求。

保存以前的结果

有时,您可能希望下一个承诺中有一个先前承诺的结果。 这可以通过Swift元组来实现。 想象一下一个登录序列,在该序列中,您首先登录,然后想将用户名和访问令牌一起存储。 您可以这样进行:

分支链

许诺的一个有趣的部分是它们只有一个执行。 如果将两个不同的链添加到一个promise,它将仅执行一次,但是对于一个执行,您可以有两个不同的流程(其中还包括catch()):

消除

与Operations的区别(正如我在上一篇文章中介绍的)是,promise中没有内置取消机制。 要拥有此选项,我们必须实现它:

用户界面

显示ViewController有点像包装委托。 不同之处在于您自己存储承诺。 而且,主持人必须知道如何展示自己和解散自己,这是一种不幸。 不过,如果您想将所有内容包装到promise中,可以选择这样做:

结论

这次,我们研究了Promise作为掌握并发的一种方法。 它在操作上有优点,但也有缺点。 现在有些人会开始争论状态,当使用承诺时,状态可能会更好,但是我认为,这不是使用承诺的理由。 它们提高了可读性并使控制并发变得更容易。 我认为他们隐藏的太多了,因为程序员需要意识到他们在做什么,而不希望框架能够解决所有的错误。

与promise合作已经有一段时间了,我可以说,只要您知道Promises是什么,它就可以使对代码的推理更加容易。 如果您不这样做,则承诺仍然比操作容易推断。

最后一件事,因为这不是语言功能,所以它与承诺的其他实现(例如ECMA-Script版本)不同。