APIが1つの完了ハンドラーを提供するか、成功/失敗ブロックのペアを提供するかは、主に個人的な好みの問題です。
どちらのアプローチにも長所と短所がありますが、わずかな違いしかありません。
たとえば、1つの完了ハンドラーが最終結果または潜在的なエラーを結合するパラメーターを1つしか持たない場合など、さらにバリエーションがあることを考慮してください。
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のカテゴリには、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
上記で定義されているように、すべてのシナリオを処理するのに十分な汎用性があります。
ただし、非同期タスクが呼び出しサイトに完了通知を通知する他の方法があります。
約束
「未来」、「延期」、「遅延」とも呼ばれる約束は、非同期タスクの最終結果を表します(wiki Futures and promisesも参照)。
当初、約束は「保留」状態です。つまり、その「値」はまだ評価されておらず、まだ利用可能ではありません。
Objective-Cでは、Promiseは、以下に示すように非同期メソッドから返される通常のオブジェクトになります。
- (Promise*) doSomethingAsync;
!Promiseの初期状態は「保留中」です。
一方、非同期タスクは結果を評価し始めます。
また、完了ハンドラはありません。代わりに、Promiseは、呼び出しサイトが非同期タスクの最終結果を取得できる、より強力な手段を提供します。これについては、後ほど説明します。
promiseオブジェクトを作成した非同期タスクは、最終的にそのpromiseを「解決」しなければなりません。つまり、タスクは成功または失敗する可能性があるため、評価結果を渡すプロミスを「実行」するか、失敗の理由を示すエラーを渡すプロミスを「拒否」する必要があります。
!タスクは最終的にその約束を解決しなければなりません。
Promiseが解決されると、その値を含め、その状態を変更できなくなります。
!Promiseは一度しか解決できません。
約束が解決されると、呼び出しサイトは結果を取得できます(失敗したか成功したか)。これがどのように達成されるかは、約束が同期スタイルを使用して実装されるか非同期スタイルを使用して実装されるかによって異なります。
Promiseは、同期スタイルまたは非同期スタイルで実装でき、それぞれブロッキングまたは非ブロッキングのセマンティクスになります。
Promiseの値を取得するための同期スタイルでは、呼び出しサイトは、Promiseが非同期タスクによって解決され、最終結果が利用可能になるまで、現在のスレッドをブロックするメソッドを使用します。
非同期スタイルでは、呼び出しサイトはコールバックまたはハンドラーブロックを登録し、Promiseが解決された直後に呼び出されます。
同期スタイルには、非同期タスクのメリットを事実上無効にする多くの重大な欠点があることが判明しました。標準C ++ 11ライブラリの「将来」の現在の欠陥実装に関する興味深い記事は、ここで読むことができます:Broken promises–C ++ 0x futures。
Objective-Cでは、呼び出しサイトはどのように結果を取得しますか?
まあ、おそらくいくつかの例を示すのが最善です。Promiseを実装するライブラリがいくつかあります(以下のリンクを参照)。
ただし、次のコードスニペットでは、GitHub RXPromiseで利用可能なPromiseライブラリの特定の実装を使用します。私はRXPromiseの著者です。
他の実装でも同様のAPIを使用できますが、構文にわずかでわずかな違いがある場合があります。RXPromiseは、Promise / A +仕様の Objective-Cバージョンです、JavaScript堅牢で相互運用可能な実装のためのオープンスタンダードを定義しています。
以下にリストされているすべての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には、非同期タスクが失敗したときに報告したエラーであるパラメーターエラーがあります。
両方のブロックに戻り値があります。この戻り値については、すぐに明らかになります。
RXPromiseでは、then
あるプロパティブロックを返します。このブロックには、成功ハンドラーブロックとエラーハンドラーブロックの2つのパラメーターがあります。ハンドラーは呼び出しサイトで定義する必要があります。
!ハンドラーは呼び出しサイトで定義する必要があります。
したがって、式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
場合、このプロミスでは処理されないことを意味します。
別の重要な事実は、プロパティから返されたブロックを呼び出すthen
と、Promiseが返されることです。
! then(...)
Promiseを返します
propertyから返されたブロックを呼び出すthen
と、「レシーバー」は新しいプロミス、子プロミスを返します。受信者が親プロミスになります。
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"]);
同様に、非同期タスクが失敗すると、返されたプロミスはエラーで解決(「拒否」)されます。
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);
このような「チェーン」は「継続」とも呼ばれます。
エラー処理
Promiseにより、エラーの処理が特に簡単になります。親プロミスにエラーハンドラが定義されていない場合、エラーは親から子に「転送」されます。このエラーは、子が処理するまでチェーンに転送されます。したがって、上記の鎖を有する、我々はどこにでも起こる可能性がある潜在的なエラーを扱う別の「継続」を追加することにより、単にエラー処理を実装することができます上記の。
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);
}
一般に、Promiseには他の便利な機能があります。
たとえば、then
1つを介してプロミスへの参照を使用すると、必要な数のハンドラを「登録」できます。RXPromiseでは、ハンドラーは完全にスレッドセーフであるため、いつでも、どのスレッドからでもハンドラーを登録できます。
RXPromiseには、Promise / A +仕様では必要とされない便利な機能がいくつかあります。1つは「キャンセル」です。
「キャンセル」は非常に重要で重要な機能であることが判明しました。たとえば、Promiseへの参照を保持している呼び出しサイトcancel
は、最終的な結果に関心がなくなったことを示すために、Promiseにメッセージを送信できます。
Webから画像を読み込み、View Controllerに表示される非同期タスクを想像してください。ユーザーが現在のView Controllerから離れた場合、開発者はキャンセルメッセージをimagePromiseに送信するコードを実装できます。このコードは、リクエストがキャンセルされるHTTPリクエストオペレーションで定義されたエラーハンドラーをトリガーします。
RXPromiseでは、キャンセルメッセージは親から子にのみ転送されますが、その逆は転送されません。つまり、「ルート」プロミスは、すべての子プロミスをキャンセルします。ただし、子プロミスは、親である「ブランチ」のみをキャンセルします。約束が既に解決されている場合、キャンセルメッセージも子に転送されます。
非同期タスクは、それ自体のプロミスのハンドラを登録できるため、他の誰かがそれをキャンセルしたことを検出できます。その後、時間のかかる高コストなタスクの実行が途中で停止する場合があります。
GitHubで見つかったObjective-CのPromiseの実装は他にもいくつかあります。
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。
このリストは完全ではない可能性があります!
プロジェクト用に3番目のライブラリを選択するときは、ライブラリの実装が以下に示す前提条件に従っているかどうかを慎重に確認してください。
信頼できるプロミスライブラリはスレッドセーフである必要があります。
それはすべて非同期処理に関するものであり、複数のCPUを利用し、可能な限り異なるスレッドで同時に実行したいと考えています。注意してください、ほとんどの実装はスレッドセーフではありません!
ハンドラーは非同期で呼び出す必要がありますは、呼び出しサイトに関して必要があります!常に、そして何であれ!
非同期関数を呼び出す場合、適切な実装も非常に厳密なパターンに従う必要があります。多くの実装者は、ケースを「最適化」する傾向があります。この場合、ハンドラーが登録されるときにプロミスが既に解決されているときに、ハンドラーが同期的に呼び出されます。これはあらゆる種類の問題を引き起こす可能性があります。参照Zalgoを放出しません!。
約束をキャンセルするメカニズムも必要です。
非同期タスクをキャンセルする可能性は、多くの場合、要件分析で優先度の高い要件になります。そうでない場合は、アプリがリリースされてからしばらくして、ユーザーからの改善要求が確実に提出されます。理由は明らかである必要があります:停止したり、終了に時間がかかりすぎたりする可能性のあるタスクは、ユーザーまたはタイムアウトによってキャンセルできる必要があります。適切なpromiseライブラリはキャンセルをサポートする必要があります。