转向漂亮的异步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
- 隐藏的危险,请参阅“显示危险”
接口和实现都可以。 但是,这两种方法都存在一些危险。 让我们来揭示它。