web-dev-qa-db-ja.com

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

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

どちらが望ましいパターンですか?長所と短所は何ですか?いつどちらを使用しますか?

24
Jeffery Thomas

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

8
user79302

APIがone完了ハンドラーを提供するか、pair成功/失敗ブロックを提供するかは、主に個人の問題です好み。

どちらのアプローチにも長所と短所がありますが、違いはわずかです。

one完了ハンドラーにoneパラメーター結合最終的な結果または潜在的なエラー:

_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_は、すべてのシナリオを処理するのに十分一般的で十分です。

ただし、非同期タスクがその完了通知を呼び出しサイトに通知する他の方法があります。

約束

「Futures」、「Deferred」、「Delayed」とも呼ばれるプロミスは、非同期タスクのeventual結果を表します(wiki Futures and promises も参照) )。

最初は、promiseは「保留中」の状態です。つまり、「価値」はまだ評価されておらず、まだ利用できません。

Objective-Cでは、Promiseは次のように非同期メソッドから返される通常のオブジェクトです。

_- (Promise*) doSomethingAsync;
_

Promiseの初期状態は「保留中」です。

一方、非同期タスクはその結果の評価を開始します。

また、完了ハンドラがないことに注意してください。代わりに、Promiseは、呼び出しサイトが非同期タスクの最終的な結果を取得できる、より強力な手段を提供します。

Promiseオブジェクトを作成した非同期タスクは、最終的にはpromiseを「解決」する必要があります。つまり、タスクは成功または失敗する可能性があるため、評価結果を渡す約束を「満たす」か、失敗の理由を示すエラーを渡す約束を「拒否」する必要があります。

タスクは最終的にその約束を解決する必要があります。

Promiseが解決されると、その値を含め、Promiseの状態を変更できなくなります。

プロミスは1回だけ解決できます。

Promiseが解決されると、呼び出しサイトは(失敗したか成功したかにかかわらず)結果を取得できます。これがどのように達成されるかは、Promiseが同期スタイルと非同期スタイルのどちらを使用して実装されているかによって異なります。

Promiseは同期または非同期のスタイルで実装でき、blockingまたはnon-blockingのいずれかのセマンティクスになります。

同期スタイルでは、promiseの値を取得するために、呼び出しサイトは、promiseが非同期で解決されるまで現在のスレッドをblockするメソッドを使用しますタスクと最終的な結果が利用可能です。

非同期のスタイルでは、呼び出しサイトは、promiseが解決された直後に呼び出されるコールバックまたはハンドラーブロックを登録します。

同期スタイルには、非同期タスクのメリットを効果的に無効にする多くの重大な欠点があることがわかりました。標準のC++ 11 libでの現在欠陥のある「将来」の実装に関する興味深い記事は、こちらから読むことができます: Broken promises–C++ 0x futures

Objective-Cでは、呼び出しサイトはどのように結果を取得するのですか?

まあ、いくつかの例を示すのがおそらく最善でしょう。 Promiseを実装するライブラリがいくつかあります(以下のリンクを参照)。

ただし、次のコードスニペットでは、GitHub RXPromise から入手できるPromiseライブラリの特定の実装を使用します。私はRXPromiseの作者です。

他の実装でも同様のAPIが使用されている場合がありますが、構文にわずかな微妙な違いがある可能性があります。 RXPromiseは Promise/A +仕様 のObjective-Cバージョンであり、JavaScriptのpromiseの堅牢で相互運用可能な実装のオープンスタンダードを定義しています。

以下にリストされているすべてのpromiseライブラリは、非同期スタイルを実装しています。

異なる実装の間にはかなり大きな違いがあります。 RXPromiseは内部的にディスパッチlibを利用し、完全にスレッドセーフで、非常に軽量で、キャンセルなどの便利な機能も多数備えています。

呼び出しサイトは、「登録」ハンドラを通じて非同期タスクの最終結果を取得します。 「Promise/A +仕様」では、メソッドthenを定義しています。

メソッドthen

RXPromiseでは、次のようになります。

_promise.then(successHandler, errorHandler);
_

ここで、successHandlerは、promiseが「満たされた」ときに呼び出されるブロックであり、errorHandlerは、約束は「拒否されました」。

thenは、最終的な結果を取得し、成功またはエラーハンドラーを定義するために使用されます。

RXPromiseでは、ハンドラーブロックに次のシグネチャがあります。

_typedef id (^success_handler_t)(id result);
typedef id (^error_handler_t)(NSError* error);
_

Success_handlerにはパラメーターresultがあり、これは明らかに非同期タスクの最終的な結果です。同様に、error_handlerにはパラメーターerrorがあり、これは非同期タスクが失敗したときに非同期タスクによって報告されたエラーです。

どちらのブロックにも戻り値があります。この戻り値については、すぐに明らかになります。

RXPromiseでは、thenはブロックを返すpropertyです。このブロックには、成功ハンドラブロックとエラーハンドラブロックの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です。つまり、エラーが発生した場合、このプロミスでは処理されません。

もう1つの重要な事実は、プロパティthenから返されたブロックを呼び出すと、Promiseが返されることです。

then(...)はPromiseを返します

プロパティthenから返されたブロックを呼び出すと、「レシーバー」はnewPromise、childpromiseを返します。レシーバーはparent約束になります。

_RXPromise* rootPromise = asyncA();
RXPromise* childPromise = rootPromise.then(successHandler, nil);
assert(childPromise.parent == rootPromise);
_

どういう意味ですか?

これにより、効果的に順次実行される非同期タスクを「チェーン」できます。

さらに、いずれかのハンドラの戻り値は、返されたpromiseの「値」になります。したがって、タスクが成功し、最終的な結果が@“ OK”の場合、返されるプロミスは値が““ 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]]);
_

ハンドラーは別のpromiseを返すこともあります。たとえば、そのハンドラーが別の非同期タスクを実行するとします。このメカニズムにより、非同期タスクを「チェーン」できます。

_RXPromise* returnedPromise = asyncA().then(^id(id result){
    return asyncB(result);
}, nil);
_

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

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

より複雑な例:

ここでは、asyncTaskAasyncTaskBasyncTaskCおよびasyncTaskDsequentiallyを実行します-以降の各タスクは入力としての前のタスクの結果:

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

このような「連鎖」は「継続」とも呼ばれます。

エラー処理

プロミスは、エラーの処理を特に簡単にします。親プロミスにエラーハンドラーが定義されていない場合、エラーは親から子に「転送」されます。子が処理するまで、エラーはチェーンの上に転送されます。したがって、上記のチェーンがあれば、どこでも発生する可能性のある潜在的なエラーを処理する別の「継続」を追加するだけでエラー処理を実装できますabove

_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を介してpromiseへの参照があると、必要なだけハンドラーを「登録」できます。 RXPromiseでは、ハンドラーの登録は完全にスレッドセーフであるため、いつでも、どのスレッドからでも発生する可能性があります。

RXPromiseには、Promise/A +仕様では不要な、さらに便利な機能がいくつかあります。一つは「キャンセル」です。

「キャンセル」は非常に重要で重要な機能であることが判明しました。たとえば、promiseへの参照を保持している呼び出しサイトは、cancelメッセージを送信して、最終的な結果に関心がないことを示すことができます。

Webから画像を読み込み、View Controllerに表示される非同期タスクを想像してみてください。ユーザーが現在のビューコントローラーから離れた場合、開発者はキャンセルメッセージをimagePromiseに送信するコードを実装できます。これにより、HTTPリクエストオペレーションで定義されたエラーハンドラーがトリガーされます。リクエストがキャンセルされる場所。

RXPromiseでは、キャンセルメッセージは親から子にのみ転送され、その逆は転送されません。つまり、「根本的な」約束は、すべての子供の約束をキャンセルします。しかし、子供の約束はそれが親である「ブランチ」をキャンセルするだけです。約束がすでに解決されている場合、キャンセルメッセージも子供に転送されます。

非同期タスクはitself自身のpromiseのハンドラーを登録できるため、他の誰かがキャンセルしたことを検出できます。その後、時間とコストがかかる可能性のあるタスクの実行が途中で停止する可能性があります。

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番目のライブラリを選択するときは、ライブラリの実装が以下にリストされている前提条件に従っているかどうかを慎重に確認してください。

  • 信頼できるpromiseライブラリはスレッドセーフである必要があります。

    それはすべて非同期処理に関するものであり、複数のCPUを利用して、可能な限り異なるスレッドで同時に実行したいと考えています。注意してください、ほとんどの実装はスレッドセーフではありません!

  • ハンドラは、呼び出しサイトに関連して非同期で呼び出される必要があります。常に、そして何があっても!

    まともな実装も、非同期関数を呼び出すときに非常に厳密なパターンに従う必要があります。多くの実装者は、ハンドラーが登録されるときにpromiseがすでに解決されているときにハンドラーが同期的に呼び出される場合を「最適化」する傾向があります。これは、あらゆる種類の問題を引き起こす可能性があります。 Zalgoをリリースしないでください! を参照してください。

  • 約束をキャンセルするメカニズムも必要です。

    非同期タスクをキャンセルする可能性は、要件分析で優先度の高い要件になることがよくあります。そうでない場合は、アプリのリリース後しばらくしてからユーザーからの機能強化リクエストが確実に提出されます。理由は明らかです。停止したり、完了に時間がかかりすぎたりする可能性のあるタスクは、ユーザーまたはタイムアウトによってキャンセルできる必要があります。まともなpromiseライブラリはキャンセルをサポートする必要があります。

8
CouchDeveloper

これは古い質問だと思いますが、私の答えは他の質問と異なるため、答える必要があります。

それは個人的な好みの問題だと言う人のために、私は同意する必要があります。どちらか一方を優先するのに十分で論理的な理由があります...

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

成功ブロックと失敗ブロックを分離すると、コードを静的に検証できます。


Swiftでは状況が変わることに注意してください。その中で、Either enumの概念を実装して、単一の完了ブロックがオブジェクトまたはエラーのいずれかを持つことが保証され、それらの1つだけが必要になるようにすることができます。したがって、Swiftの場合、単一のブロックの方が適しています。

3
Daniel T.

個人的な好みになるのではないかと思います...

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

このような入れ子の比較的極端な例として、このパターンを示す これはRubyです です。

1
Frank Shearar

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

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

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

しばらく気を抜くと思います。

0
Jeffery Thomas