成功:/失败:阻止与完成:阻止


23

我在Objective-C中看到了两种常见的块模式。一个是一对成功:/失败:块,另一个是单个完成:块。

例如,假设我有一个任务将异步返回对象,而该任务可能会失败。第一种模式是-taskWithSuccess:(void (^)(id object))success failure:(void (^)(NSError *error))failure。第二种模式是-taskWithCompletion:(void (^)(id object, NSError *error))completion

成功:/失败:

[target taskWithSuccess:^(id object) {
    // W00t! I've got my object
} failure:^(NSError *error) {
    // Oh noes! report the failure.
}];

完成:

[target taskWithCompletion:^(id object, NSError *error) {
    if (object) {
        // W00t! I've got my object
    } else {
        // Oh noes! report the failure.
    }
}];

哪个是首选模式?优点和缺点是什么?您什么时候可以使用另一个?


我敢肯定,Objective-C可以使用throw / catch进行异常处理,是否有不能使用的理由?
FrustratedWithFormsDesigner 2013年

这些方法均允许链接异步调用,但异常不会给您。
Frank Shearar 2013年

5
@FrustratedWithFormsDesigner:stackoverflow.com/a/3678556/2289-惯用objc不使用try / catch进行流控制。
2013年

1
请考虑将您的答案从问题移到答案...毕竟,这是一个答案(您可以回答自己的问题)。

1
我终于屈服于同伴的压力,并将答案转移到实际答案。
杰弗里·托马斯

Answers:


8

完成回调(与成功/失败对相对)更为通用。如果需要在处理返回状态之前准备一些上下文,则可以在“ if(object)”子句之前进行操作。在成功/失败的情况下,您必须复制此代码。当然,这取决于回调语义。


无法评论原始问题...异常不是Objective-C中的有效流控制(很好,可可),因此不应这样使用。应该捕获引发的异常只是为了正常终止。

是的,我可以看到。如果-task…可以返回对象,但对象未处于正确状态,那么在成功情况下您仍然需要错误处理。
Jeffery Thomas

是的,如果块不是就位,而是作为参数传递给您的控制器,则您必须将两个块折腾。当回调需要通过许多层传递时,这可能很无聊。不过,您始终可以将其拆分/重新组合。

我不明白补全处理程序是如何通用的。完成基本上将多种方法参数变成一种-以块参数的形式。另外,泛型意味着更好吗?在MVC中,您有时也经常在视图控制器中有重复的代码,由于关注点分离,这是必不可少的。我认为这并不是远离MVC的理由。
Boon 2014年

@Boon我认为单个处理程序更为通用的原因之一是,在某些情况下,您希望被调用方/处理程序/块本身确定操作是成功还是失败。考虑部分成功的情况,其中您可能有一个带有部分数据的对象,而您的错误对象是一个错误,指示未返回所有数据。该块可以检查数据本身,并检查其是否足够。对于二进制成功/失败回调方案,这是不可能的。
特拉维斯

8

我想说,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);

处理程序块的返回值成为子诺言的值。

如果没有子承诺,则返回值无效。

一个更复杂的示例:

在这里,我们执行asyncTaskAasyncTaskBasyncTaskCasyncTaskD 顺序地 -和每个后续任务将前述任务作为输入的结果:

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!

  • 还应该有一种取消承诺的机制。

    在需求分析中,取消异步任务的可能性通常成为具有高优先级的需求。如果没有,请确保在发布应用程序后的一段时间后,用户会提出增强请求。原因应该很明显:任何可能停滞或完成时间太长的任务,应由用户或超时取消。体面的诺言库应支持取消。


1
这获得了有史以来最长的无答案奖。但是,一种努力:-)
旅行的人

3

我意识到这是一个老问题,但我必须回答,因为我的回答与其他问题有所不同。

对于那些说这是个人喜好的问题,我必须不同意。有一个很好的,合乎逻辑的理由来偏爱一个...

在完成情况下,您的块有两个对象,一个代表成功,而另一个代表失败……那么,如果两个都为零,该怎么办?如果两者都有价值,您该怎么办?这些是在编译时可以避免的问题,因此应该避免。您可以通过使用两个单独的块来避免这些问题。

具有独立的成功和失败块可以使您的代码静态可验证。


请注意,Swift会改变一切。在其中,我们可以实现Either枚举的概念,以确保单个完成块具有一个对象或一个错误,并且必须恰好具有其中之一。因此,对于Swift而言,单个块更好。


1

我怀疑这最终将成为个人喜好...

但是我更喜欢单独的成功/失败模块。我喜欢分离成功/失败逻辑。如果嵌套了成功/失败,您最终会得到一些更具可读性的东西(至少在我看来)。

作为此类嵌套的一个相对极端的示例,下面是一些显示这种模式的Ruby


1
我已经看到了两者的嵌套链。我认为它们看起来都很糟糕,但这是我的个人看法。
Jeffery Thomas

1
但是您还能如何链接异步调用?
Frank Shearar 2013年

我不认识男人……我不知道。我要问的部分原因是因为我不喜欢任何异步代码的外观。
Jeffery Thomas

当然。您最终以连续传递样式编写代码,这并不奇怪。(正是由于这个原因,Haskell才使用了符号:让您以表面上直接的风格进行书写。)
Frank Shearar 2013年

您可能对此ObjC Promises实现感兴趣:github.com/couchdeveloper/RXPromise
e1985 2013年

0

这感觉像是一个完整的解决方案,但是我认为这里没有正确的答案。我之所以选择完成模块,是因为在使用成功/失败模块时,在成功条件下可能仍需要进行错误处理。

我认为最终代码看起来像

[target taskWithCompletion:^(id object, NSError *error) {
    if (error) {
        // Oh noes! report the failure.
    } else if (![target validateObject:&object error:&error]) {
        // Oh noes! report the failure.
    } else {
        // W00t! I've got my object
    }
}];

或简单地

[target taskWithCompletion:^(id object, NSError *error) {
    if (error || ![target validateObject:&object error:&error]) {
        // Oh noes! report the failure.
        return;
    }

    // W00t! I've got my object
}];

不是最好的代码块,并且嵌套起来会变得更糟

[target taskWithCompletion:^(id object, NSError *error) {
    if (error || ![target validateObject:&object error:&error]) {
        // Oh noes! report the failure.
        return;
    }

    [object objectTaskWithCompletion:^(id object2, NSError *error) {
        if (error || ![object validateObject2:&object2 error:&error]) {
            // Oh noes! report the failure.
            return;
        }

        // W00t! I've got object and object 2
    }];
}];

我想我会拖一会儿。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.