成功:/失敗:ブロックvs完了:ブロック


23

Objective-Cのブロックには2つの一般的なパターンがあります。1つはsuccess:/ failure:ブロックのペアであり、もう1つは単一のcompletion:ブロックです。

たとえば、非同期でオブジェクトを返すタスクがあり、そのタスクが失敗する可能性があるとしましょう。最初のパターンは-taskWithSuccess:(void (^)(id object))success failure:(void (^)(NSError *error))failureです。2番目のパターンは-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

これらのいずれかは、非同期呼び出しのチェーンを許可しますが、例外はあなたに与えません。
フランク・シーラー

5
@FrustratedWithFormsDesigner:stackoverflow.com/a/3678556/2289-慣用的なオブジェクトは、フロー制御にtry / catchを使用しません。
アント

1
Answerを質問から回答に移動することを検討してください。結局のところ、それは回答です(そして、あなた自身の質問に答えることができます)。

1
私はついに仲間の圧力に屈し、私の答えを実際の答えに変えました。
ジェフリートーマス

回答:


8

完了コールバック(成功/失敗ペアとは反対)はより一般的です。戻りステータスを処理する前に何らかのコンテキストを準備する必要がある場合は、「if(オブジェクト)」句の直前に準備できます。成功/失敗の場合、このコードを複製する必要があります。もちろん、これはコールバックのセマンティクスに依存します。


元の質問にコメントすることはできません...例外は、objective-c(まあ、ココア)で有効なフロー制御ではないため、そのまま使用しないでください。スローされた例外は、正常に終了するためにのみキャッチする必要があります。

ええ、私はそれを見ることができます。場合は-task…、オブジェクトを返しますが、オブジェクトが正しい状態ではありませんでした、そして、あなたはまだ成功条件でのエラー処理が必要になります。
ジェフリートーマス

ええ、ブロックがインプレースではなく、コントローラーに引数として渡される場合は、2つのブロックを投げる必要があります。コールバックを多くのレイヤーに渡す必要がある場合、これは退屈かもしれません。ただし、いつでも元に戻すことができます。

完了ハンドラーがより一般的である方法がわかりません。完了により、基本的に複数のメソッドパラメーターが1つに変わります-ブロックパラメーターの形式です。また、ジェネリックはより良い意味ですか?MVCでは、View Controllerでもコードが重複していることがよくありますが、これは懸念の分離のために必要な悪です。しかし、それがMVCから遠ざかる理由ではないと思います。
ブーン14年

@Boon単一のハンドラーがより一般的であると思う理由の1つは、操作が成功したか失敗したかを、呼び出し先/ハンドラー/ブロック自体で判断する場合です。部分的なデータを持つオブジェクトが存在する可能性があり、エラーオブジェクトがすべてのデータが返されたわけではないことを示すエラーである部分的な成功の場合を考えてください。ブロックはデータ自体を調べて、データが十分かどうかを確認できます。これは、バイナリ成功/失敗コールバックシナリオでは不可能です。
トラビス

8

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);

ハンドラーブロックの戻り値は、子プロミスの値になります。

子プロミスがない場合、戻り値は効果がありません。

より複雑な例:

ここで、我々は、実行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);

このような「チェーン」は「継続」とも呼ばれます。

エラー処理

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には他の便利な機能があります。

たとえば、then1つを介してプロミスへの参​​照を使用すると、必要な数のハンドラを「登録」できます。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ライブラリはキャンセルをサポートする必要があります。


1
これは、これまで最長の非回答に対して賞を獲得します。しかし、努力のためのA :
旅行人

3

これは古い質問であることに気づきましたが、私の答えは他のものとは異なるため、答えなければなりません。

個人的な好みの問題だと言う人のために、私は反対しなければなりません。あるものを他のものよりも好む論理的な理由があります...

完了の場合、ブロックには2つのオブジェクトが渡されます。1つは成功を表し、もう1つは失敗を表します...では、両方がゼロの場合はどうしますか?両方に価値がある場合はどうしますか?これらは、コンパイル時に回避できる質問なので、そうすべきです。これらの質問を回避するには、2つの独立したブロックを使用します。

成功と失敗のブロックを分けておくと、コードを静的に検証できます。


Swiftによって状況が変わることに注意してください。その中に、Eitherenum の概念を実装して、単一の完了ブロックにオブジェクトまたはエラーがあることを保証し、それらのうちの1つを正確に持たせる必要があります。したがって、Swiftの場合、単一のブロックの方が優れています。


1

私はそれが個人的な好みになると思う...

しかし、成功/失敗のブロックを別にしたほうがいいです。成功/失敗のロジックを分離するのが好きです。入れ子になった成功/失敗がある場合、(少なくとも私の意見では)より読みやすいものになるでしょう。

このようなネストの比較的極端な例として、このパターンを示すRubyがあります。


1
私は両方のネストされたチェーンを見てきました。どちらもひどいように見えますが、それは私の個人的な意見です。
ジェフリートーマス

1
しかし、非同期呼び出しを他にどのように連鎖させることができますか?
フランク・シーラー

私は人を知りません…私は知りません。私が尋ねている理由の一部は、非同期コードがどのように見えるかが好きではないからです。
ジェフリートーマス

確かに。コードを継続渡しのスタイルで書くことになりますが、これは驚くべきことではありません。(Haskellには、まさにこの理由で表記法があります。表面的に直接的なスタイルで記述できるようにするためです。)
フランクシェラー

このObjC Promisesの実装に興味があるかもしれません:github.com/couchdeveloper/RXPromise
e1985

0

これは完全なcopoutのように感じますが、ここに正しい答えがあるとは思いません。成功/失敗ブロックを使用する場合、成功条件でエラー処理を行う必要があるため、完了ブロックを使用しました。

最終的なコードは次のようになると思います

[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
    }];
}];

私はしばらくの間行くつもりだと思います。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.