转向漂亮的异步Swift代码

本文提高了对与异步代码相关的问题的认识,并提供了在Swift 3.0–3.1上进行编程时解决这些问题的示例。

样本问题的描述

这是源数据:

  • Person是包含有关人员信息的结构的示例。
  • MyService是用作模型入口点的类的示例。
  • MyViewController是管理与UI相关的实例的类的示例。

MyService必须将Person提供给MyViewController以返回具有相应标识符的请求。 它可能在内存中没有所请求的信息,因此获取人员数据可能涉及网络,磁盘操作等。

回到同步编码时代

我注意到许多项目仍然使用同步方法。 因此,让我们首先使用它来解决示例问题。

 扩展MyService { 
func person(identifier:String)throws-> Person? {
返回/ *从网络获取人员* /
}
}

似乎非常简单: input arguments -> output result 。 此方法可以返回该人(如果没有这样的人,则为nil);如果出了问题,则抛出一个问题。

它的使用方式就是这样:

 扩展MyViewController { 
func present(personWithID标识符:字符串){
  / *不要忘记调度到后台队列* / 
DispatchQueue.global()。async {
做{
让人=尝试self.myService
.person(标识符:标识符)
  / *不要忘记调度到主队列* / 
DispatchQueue.main.async {
self.present(人:人)
}
} {
  / *不要忘记调度到主队列* / 
DispatchQueue.main.async {
self.present(错误:错误)
}
}
}
}
}

如您所见,此方法的用法看起来不如界面美观。

关于“不要忘记”的评论

恕我直言 ,每个“不要忘记”的评论都指向一个糟糕的体系结构。 即使您是可以在99%的情况下避免错误的机器人,但具有100个此类调用的应用程序至少会遇到一个关键问题。

在更实际的条件下,此类调用通常是嵌套或并行化的,这使代码量,复杂性和出错机会增加了三倍。 此外, MyService可能出现的死锁还有待讨论。

关于死锁

死锁是编程中的噩梦,发生在最意想不到的地方和最不可思议的情况下。 更糟糕的是,根据我自己的经验,我可以说生产中发现了80%的死锁。

MyService的角度来看,上面的代码是同步的。 执行func person(identifier: String) throws -> Person? ,我们必须至少使用两次锁。 因此,现实世界中的问题大大增加了此类案件的复杂性。

有两种可能的解决方案:要么100%专注并谨慎,要么不使用存在如此巨大问题的方法。 您可能已经猜到了,我们将探讨选项2。

摘要:同步方法

优点

  • MyService接口和实现看起来很简单

缺点

  • MyService出现死锁的可能性
  • “不要忘记” x3
  • 隐藏的危险,请参阅“显示危险”

新方法的验收标准

现在,让我们尝试找到一种新的编码方法,该方法可以消除同步方法中的所有问题。 此方法必须符合以下接受标准:

  • 没有僵局
  • 没有“不要忘记”的
  • 将UI和模型粘合在一起的可靠方法。

尝试1.0。 带回调的异步代码

从OS X 10.6和iOS 4.0开始,我们可以使用闭包(也称为块)作为回调,这为进行异步流打开了另一个维度。

 扩展MyService { 
func person(标识符:字符串,
回调:@转义(人员?,错误?)->无效){
self.internalQueue.async {
让人员= / *从网络获取人员* /
  / *不要忘记在此处添加回调的调用* / 
回调(人,无)
}
}
}

因此,我们将回调作为最后一个参数传递。 该界面看起来比以前的界面难看。 它看起来更像是一个纯函数,但现在不是。 但是,让我们检查一下它的使用情况。

 扩展MyViewController { 
func present(personWithID标识符:字符串){
self.myService.person(identifier:标识符){
(人,错误)在
  / *不要忘记调度到主队列* / 
DispatchQueue.main.async {
 如果让错误=错误{ 
self.present(错误:错误)
}其他{
self.present(人:人)
}
  } 
}
}
}

圈复杂度提高🙁

如果您需要 在各处 添加 weaks ,请转到“揭示危险”

摘要:尝试1.0。 带回调的异步代码

优点

  • 一个“不要忘记”的问题
  • 没有僵局

缺点

  • 一种新的“不要忘记”
  • 方法输出列为参数
  • “不要忘记” x2
  • 隐藏的危险,请参阅“显示危险”

尝试2.0。 期货

期货的概念是分开发展的,而且很棒,特别是与停业交易相结合时。

< Wikipedia >…(未来)描述了一个对象,该对象充当最初未知的结果的代理,通常是因为其值的计算尚未完成。

这是比前一种方法更高级的方法,因此,如果您不熟悉此想法,请确保阅读代码下面的解释。

 扩展MyService { 
func person(identifier:String)-> Future {
返回未来(执行者:.queue(self.internalQueue)){
返回/ *从网络获取人员* /
}
}
}

简要说明

调用 future(executor: ...) { ... } 函数执行以下操作:

1.返回Future

2.使用指定的执行程序异步执行关闭。 从闭包返回值将导致将来完成。

执行程序是一种抽象,它基本上描述了可以执行块的对象,例如 DispatchQueue NSManagedObjectContext 等。

因此,我们分派了“从网络获取人”的执行工作,并返回了将来。

 扩展MyViewController { 
func present(personWithID标识符:字符串){
self.myService.person(identifier:标识符)
  / *不要忘记调度到主队列* / 
.onComplete(executor:.main){
(personOrError)->无效
 切换personOrError { 
案例。成功(让个人):
self.present(人:人)
案例。失败(让错误):
self.present(错误:错误)
}
  } 
}
}

简要说明

调用 self.myService.person(identifier: identifier) 提供 Future .onComplete(executor: .main) { 指定对将来完成的反应。 executor: main 表示指定的闭包使用main executor aka main队列执行。 该闭包具有单个参数 Fallible Fallible 与标准库中的 Optional 几乎相同 ,不同之处 .failure(Error) 它具有 .failure(Error) 情况而不是 .none 因此,我们 通过在两个之间切换来 显示一个人( Person )或一个错误可用案例。

摘要:尝试2.0。 期货

优点

  • MyService接口和实现看起来很简单
  • 2“别忘了”的修正
  • 没有僵局

缺点

  • 还有一个图书馆
  • “不要忘记” x1
  • 隐藏的危险,请参阅“显示危险”

接口和实现都可以。 但是,这两种方法都存在一些危险。 让我们来揭示它。