@Zhangの優れた回答に加えて、OPが直面している一般的な問題と、この一般的な問題に対する「一般的な解決策」がどのように見えるかについて説明したいと思います。
共通の目的は次のとおりです。
サーバーからアイテムのリストを取得します。各アイテムには、他のリソース (画像など) を指す URL が含まれています。
リストを受け取ったら、リスト内の各項目について、URL で指定されたリソース (画像) をフェッチします。
これを同期スタイルで実装すると、解決策は明らかで、実際には非常に簡単です。しかし、ネットワーキングを行う際に好まれる方法である非同期スタイルを採用する場合、そのような問題を解決する方法を知らない限り、実行可能なソリューションは驚くほど複雑になります;)
ここで興味深いのは #2 です。パート 1 は、非同期呼び出しと、完了関数がパート 2 を呼び出す完了関数を介して簡単に実行できます。
物事をより理解しやすくするために、いくつかの単純化といくつかの前提条件を作成します。
パート 1 では、要素のリスト、たとえば要素NSArray
を含むオブジェクトを取得します。各要素には、別のリソースへの URL であるプロパティがあります。
ここで、ループ内で次々と非同期に処理される N 個の入力値を表す要素の配列が既にあると簡単に仮定できます。その配列に「ソース配列」という名前を付けましょう。
非同期メソッド/関数を扱います。何かの処理が非同期で終了したことをメソッド/関数に通知させる 1 つの方法は、完了ハンドラー (ブロック) です。
すべての完了ハンドラの共通シグネチャは次のように定義されます。
typedef void (^completion_t)(id result);
注: resultは、非同期関数またはメソッドの最終的な結果を表すものとします。NSError
これは、私たちが期待する種類のもの (画像など) である場合もあれば、パスやオブジェクトなどのエラーを示している場合もあります。
パート 2 を実装するには、入力 (入力配列の 1 つの要素) を受け取り、出力を生成する非同期メソッド/関数が必要です。これは、「画像リソースの取得」タスクに対応します。後で、パート 1 で取得した「入力配列」の各要素にこのメソッド/関数を適用する必要があります。
一般的な関数である「変換関数」には、次のシグネチャがあります。
void transform(id input, completion_t completion);
対応するメソッドには、次の署名があります。
-(void) transformWithInput:(id)input
completion:(completion_t)completionHandler;
関数の typedef を次のように定義できます。
typedef void (^transform_t)(id input, completion_t completion);
変換関数またはメソッドの結果は、完了ハンドラのパラメータを介して渡されることに注意してください。同期関数は戻り値を持ち、結果を返します。
注: 「変換」という名前は単なる総称です。ネットワーク要求をメソッドにラップして、そのような種類の「変換」機能を取得できます。OP の例では、URLが入力パラメーターになり、完了ハンドラーの結果パラメーターはサーバーからフェッチされた画像 (またはエラー) になります。
注: これと次の単純化は、非同期パターンの説明を理解しやすくするために行ったものです。実際には、非同期関数またはメソッドは他の入力パラメーターを受け取る場合があり、完了ハンドラーも他のパラメーターを持つ場合があります。
さて、より「トリッキーな」部分:
非同期スタイルでループを実装する
これは、同期プログラミング スタイルとは少し「異なります」。
意図的に、この反復を行う何らかのforEach関数またはメソッドを定義します。その関数またはメソッド自体が非同期です。そして、非同期関数またはメソッドには完了ハンドラーがあることがわかりました。
したがって、関数の場合、「forEach」関数を次のように宣言できます。
`void transform_each(NSArray* inArray, transform_t task, completion_t completion);`
transform_each
入力配列inArray内の各オブジェクトに非同期変換関数タスクを順次適用します。すべての入力の処理が完了すると、完了ハンドラーのcompletionが呼び出されます。
完了ハンドラの結果パラメータは、対応する入力と同じ順序で各変換関数の結果を含む配列です。
注: ここでの「順次」とは、入力が次々に処理されることを意味します。そのパターンの変形は、入力を並行して処理する場合があります。
パラメータinArrayは、ステップ 1 で収集した「入力配列」です。
パラメータタスクは、非同期変換関数であり、事実上、入力を受け取って出力を生成するものであれば何でもかまいません。これは、OP の例からの非同期の「イメージのフェッチ」タスクになります。
パラメータ補完は、すべての入力が処理されたときに呼び出されるハンドラです。そのパラメーターには、配列内の各変換関数の出力が含まれています。
は次のtransform_each
ように実装できます。まず、「ヘルパー」関数が必要ですdo_each
。
do_each
実際には、非同期スタイルでループを実装するためのパターン全体の中心であるため、ここで詳しく見てみましょう。
void do_each(NSEnumerator* iter, transform_t task, NSMutableArray* outArray, completion_t completion)
{
id obj = [iter nextObject];
if (obj == nil) {
if (completion)
completion([outArray copy]);
return;
}
task(obj, ^(id result){
[outArray addObject:result];
do_each(iter, task, outArray, completion);
});
}
ここで興味深い部分と、(for_each 関数として) ループを実装するための "一般的な非同期パターン" または "イディオム"は、変換関数do_each
の完了ハンドラーから呼び出されることです。これは再帰のように見えるかもしれませんが、実際にはそうではありません。
パラメータiterは、処理される配列内の現在のオブジェクトを指します。また、停止条件を決定するためにも使用されます。列挙子が末尾を超えると、nil
method から結果が得られnextObject
ます。これにより、最終的にループが停止します。
それ以外の場合、現在のオブジェクトを入力パラメーターとして変換関数タスクが呼び出されます。オブジェクトは、タスクの定義に従って非同期に処理されます。完了すると、タスクの完了ハンドラが呼び出されます。そのパラメーターの結果は、変換関数の出力になります。ハンドラーは、結果の配列outArrayに結果を追加する必要があります 。次に、ヘルパーdo_each
を再度呼び出します。これは再帰呼び出しのように見えますが、実際にはそうではありません: 前者do_each
は既に返されています。これは の別の呼び出しですdo_each
。
それができたら、transform_each
以下に示すように関数を簡単に完成させることができます。
void transform_each(NSArray* inArray, transform_t task, completion_t completion) {
NSMutableArray* outArray = [[NSMutableArray alloc] initWithCapacity:[inArray count]];
NSEnumerator* iter = [inArray objectEnumerator];
do_each(iter, task, outArray, completion);
}
NSArray カテゴリ
便宜上、入力を順番に非同期に処理する「forEach」メソッドを使用して、NSArray のカテゴリを簡単に作成できます。
@interface NSArray (AsyncExtension)
- (void) async_forEachApplyTask:(transform_t) task completion:(completion_t) completion;
@end
@implementation NSArray (AsyncExtension)
- (void) async_forEachApplyTask:(transform_t) task completion:(completion_t) completion {
transform_each(self, task, completion);
}
@end
コード例は Gist にあります: transform_each
一般的な非同期パターンを解決するためのより洗練された概念は、「Futures」または「Promises」を利用することです。Objective-C の "Promise" の概念を小さなライブラリRXPromiseに実装しました。
上記の「ループ」は、RXPromise を介して非同期タスクをキャンセルする機能を含めて実装できますが、もちろんそれ以上の機能もあります。楽しむ ;)