我想说,API是提供一个完成处理程序还是一对成功/失败模块,主要是个人喜好问题。
两种方法都有优点和缺点,尽管只有一点点差异。
考虑还有其他变体,例如,一个完成处理程序可能只有一个参数结合了最终结果或潜在错误:
typedef void (^completion_t)(id result);
- (void) taskWithCompletion:(completion_t)completionHandler;
[self taskWithCompletion:^(id result){
if ([result isKindOfError:[NSError class]) {
NSLog(@"Error: %@", result);
}
else {
...
}
}];
此签名的目的是可以在其他API中通用使用完成处理程序 。
例如,在NSArray的Category中,有一个方法forEachApplyTask:completion:
可以依次为每个对象调用一个任务并中断循环IFF,从而导致错误。由于此方法本身也是异步的,因此它也具有完成处理程序:
typedef void (^completion_t)(id result);
typedef void (^task_t)(id input, completion_t);
- (void) forEachApplyTask:(task_t)task completion:(completion_t);
实际上,completion_t
上面定义的通用性足以应付所有情况。
但是,异步任务还有其他方法可以将其完成通知发给呼叫站点:
承诺
Promise(也称为“ Futures”,“ Deferred”或“ Delayed”)表示异步任务的最终结果(另请参见:Wiki Futures and promises)。
最初,承诺处于“待定”状态。也就是说,它的“值”尚未评估且尚不可用。
在Objective-C中,Promise将是一个普通对象,它将通过异步方法返回,如下所示:
- (Promise*) doSomethingAsync;
!Promise的初始状态为“待定”。
同时,异步任务开始评估其结果。
还要注意,没有完成处理程序。相反,Promise将提供一种更强大的方法,使呼叫站点可以获取异步任务的最终结果,我们将很快看到。
创建承诺对象的异步任务必须最终“解决”其承诺。这意味着,由于任务可能成功或失败,因此它必须“履行”承诺并传递评估结果给它,或者必须“拒绝”该承诺传递给它一个错误,指示失败的原因。
!一项任务必须最终实现其诺言。
解决了承诺后,便无法再更改其状态,包括其值。
!一个Promise只能解决一次。
一旦解决了诺言,呼叫站点就可以获取结果(无论失败还是成功)。如何完成此过程取决于是否使用同步或异步方式实现了承诺。
一个承诺可以在同步或异步的风格,引线被实施为任一阻塞分别非阻塞语义。
为了以同步方式获取承诺的值,调用站点将使用一种方法,该方法将阻止当前线程,直到异步任务解决了承诺并获得最终结果为止。
在异步方式中,调用站点将注册回调或处理程序块,这些请求或请求块将在解决承诺后立即被调用。
事实证明,同步样式具有许多明显的缺点,这些缺点有效地挫败了异步任务的优点。有关标准C ++ 11库中当前存在的“期货”的有缺陷的实现的有趣文章,可以在这里阅读:违背诺言– C ++ 0x期货。
在Objective-C中,呼叫站点如何获得结果?
好吧,最好显示一些例子。有几个实现Promise的库(请参见下面的链接)。
但是,对于下一个代码片段,我将使用Promise库的特定实现,该库可在GitHub RXPromise上找到。我是RXPromise的作者。
其他实现可能具有类似的API,但语法可能存在细微甚至可能细微的差异。RXPromise是Promise / A +规范的Objective-C版本,它定义了一个开放标准,用于JavaScript中的promise的健壮且可互操作的实现。
下面列出的所有Promise库都实现了异步样式。
不同的实现之间存在相当大的差异。RXPromise内部使用调度库,具有完全线程安全,轻量级的特点,并且还提供了许多其他有用的功能,例如取消。
呼叫站点通过“注册”处理程序获得异步任务的最终结果。“ Promise / A +规范”定义了方法then
。
方法 then
使用RXPromise,它看起来如下:
promise.then(successHandler, errorHandler);
其中successHandler是一个在“履行”承诺时调用的块,而 errorHandler是在“被拒绝”承诺时调用的块。
! then
用于获取最终结果并定义成功或错误处理程序。
在RXPromise中,处理程序块具有以下签名:
typedef id (^success_handler_t)(id result);
typedef id (^error_handler_t)(NSError* error);
success_handler具有参数结果,该结果显然是异步任务的最终结果。同样,error_handler具有参数error,它是异步任务失败时报告的错误。
两个块都有一个返回值。这个返回值是关于什么的,将很快变得清楚。
在RXPromise中,then
是一个返回块的属性。该块具有两个参数,成功处理程序块和错误处理程序块。处理程序必须由调用站点定义。
!处理程序必须由调用站点定义。
因此,该表达式promise.then(success_handler, error_handler);
是
then_block_t block promise.then;
block(success_handler, error_handler);
我们可以编写更简洁的代码:
doSomethingAsync
.then(^id(id result){
…
return @“OK”;
}, nil);
代码显示为:“执行doSomethingAsync,成功后,再执行成功处理程序”。
在这里,错误处理程序是nil
指在发生错误的情况下不会在此Promise中对其进行处理。
另一个重要的事实是,调用从属性then
返回的块将返回Promise:
! then(...)
返回一个承诺
当调用从property返回的块时then
,“接收者”将返回一个新的 Promise,即一个子承诺。接收者成为父母的承诺。
RXPromise* rootPromise = asyncA();
RXPromise* childPromise = rootPromise.then(successHandler, nil);
assert(childPromise.parent == rootPromise);
这意味着什么?
好吧,由于这个原因,我们可以“链接”异步任务,这些任务可以有效地按顺序执行。
此外,任一处理程序的返回值都将成为返回的诺言的“值”。因此,如果任务以最终结果@“ OK”成功完成,则返回的Promise将被“解析”(即“实现”),值为@“ OK”:
RXPromise* returnedPromise = asyncA().then(^id(id result){
return @"OK";
}, nil);
...
assert([[returnedPromise get] isEqualToString:@"OK"]);
同样,当异步任务失败时,返回的promise将被解决(即被“拒绝”)错误。
RXPromise* returnedPromise = asyncA().then(nil, ^id(NSError* error){
return error;
});
...
assert([[returnedPromise get] isKindOfClass:[NSError class]]);
处理程序还可能返回另一个诺言。例如,当该处理程序执行另一个异步任务时。通过这种机制,我们可以“链接”异步任务:
RXPromise* returnedPromise = asyncA().then(^id(id result){
return asyncB(result);
}, nil);
!处理程序块的返回值成为子诺言的值。
如果没有子承诺,则返回值无效。
一个更复杂的示例:
在这里,我们执行asyncTaskA
,asyncTaskB
,asyncTaskC
和asyncTaskD
顺序地 -和每个后续任务将前述任务作为输入的结果:
asyncTaskA()
.then(^id(id result){
return asyncTaskB(result);
}, nil)
.then(^id(id result){
return asyncTaskC(result);
}, nil)
.then(^id(id result){
return asyncTaskD(result);
}, nil)
.then(^id(id result){
// handle result
return nil;
}, nil);
这样的“链”也称为“延续”。
错误处理
承诺使处理错误特别容易。如果在父承诺中未定义错误处理程序,则错误将从父项“转发”给子项。该错误将被向上转发,直到有一个孩子处理它为止。因此,有了上述链,我们可以通过添加另一个“ continuation”来实现错误处理,该“ continuation”处理可能发生在上面任何地方的潜在错误:
asyncTaskA()
.then(^id(id result){
return asyncTaskB(result);
}, nil)
.then(^id(id result){
return asyncTaskC(result);
}, nil)
.then(^id(id result){
return asyncTaskD(result);
}, nil)
.then(^id(id result){
// handle result
return nil;
}, nil);
.then(nil, ^id(NSError*error) {
NSLog(@“”Error: %@“, error);
return nil;
});
这类似于带有异常处理的可能更熟悉的同步样式:
try {
id a = A();
id b = B(a);
id c = C(b);
id d = D(c);
// handle d
}
catch (NSError* error) {
NSLog(@“”Error: %@“, error);
}
一般而言,承诺还具有其他有用的功能:
例如,对一个承诺的引用,then
可以通过一个人“注册”所需的许多处理程序。在RXPromise中,注册处理程序可以在任何时间从任何线程进行,因为它是完全线程安全的。
RXPromise具有一些Promise / A +规范不需要的更有用的功能功能。一种是“取消”。
事实证明,“取消”是一项宝贵而重要的功能。例如,持有承诺的引用的呼叫站点可以向其发送cancel
消息,以表明它不再对最终结果感兴趣。
试想一下一个异步任务,该任务将从Web加载图像,并将其显示在视图控制器中。如果用户离开当前的视图控制器,则开发人员可以实现将取消消息发送到imagePromise的代码,该代码又触发由HTTP请求操作定义的错误处理程序,该请求将被取消。
在RXPromise中,取消消息只会从父级转发到其子级,反之则不然。也就是说,“根”承诺将取消所有儿童承诺。但是孩子的诺言只会取消父母的“分支”。如果已经解决了承诺,取消消息也将转发给孩子。
异步任务本身可以为自己的承诺注册处理程序,从而可以检测到其他人何时取消了它。然后,它可能会过早地停止执行可能耗时且昂贵的任务。
这是在GitHub上找到的Objective-C中Promises的其他两个实现:
https://github.com/Schoonology/aplus-objc
https://github.com/affablebloke/deferred-objective-c
https://github.com/bww/FutureKit
https://github.com/jkubicek/JKPromises
https://github.com/Strilanc/ObjC-CollapsingFutures
https://github.com/b52/OMPromises
https://github.com/mproberts/objc-promise
https://github.com/klaaspieter/Promise
https: //github.com/jameswomack/Promise
https://github.com/nilfs/promise-objc
https://github.com/mxcl/PromiseKit
https://github.com/apleshkov/promises-aplus
https:// github.com/KptainO/Rebelle
和我自己的实现:RXPromise。
此列表可能不完整!
为您的项目选择第三个库时,请仔细检查该库的实现是否符合下面列出的先决条件:
一个可靠的承诺库应该是线程安全的!
一切都与异步处理有关,我们希望利用多个CPU并尽可能在不同线程上同时执行。注意,大多数实现都不是线程安全的!
必须相对于调用站点异步调用处理程序!永远,无论如何!
调用异步函数时,任何体面的实现也应遵循非常严格的模式。许多实现者倾向于“优化”这种情况,即当处理程序将在注册时已经解决了promise时,将同步调用处理程序。这可能会导致各种问题。请参阅不要释放Zalgo!。
还应该有一种取消承诺的机制。
在需求分析中,取消异步任务的可能性通常成为具有高优先级的需求。如果没有,请确保在发布应用程序后的一段时间后,用户会提出增强请求。原因应该很明显:任何可能停滞或完成时间太长的任务,应由用户或超时取消。体面的诺言库应支持取消。