この記事の内容は古くなっていますので、新しい解説記事を書きました(2013/5/8):
Future を使いこなす
Dartでは、「まだ受け取っていない(未来の)値をどのように処理するか」を記述することができます。
まずは逐次型処理から考えてみましょう。ひとつの処理が終わってから次の処理を行う逐次型処理は、次のように記述できます。
void main() {
// 処理1
// 処理2
}
このプログラムは、処理1が終わってから処理2が実行されます。処理2が処理1の結果を受け取って処理される(処理2は処理1の結果に依存している)のであれば、この順で書くしかありません。たとえば次のような例が考えられます。
void main() {
double a, b;
a = Math.random(); // 処理1
b = a * 2; // 処理2
}
このプログラムは、処理1の結果であるaの値が定まらないと、処理2の結果であるbの値も定まりません。
ここで、処理1と処理2を別々に書きたい場合があります。つまり、例えば次のように書きたいということです。
void main() {
// 処理2
// 処理1
}
もしこのように書けるのであれば、処理1を別の関数やクラスに追い出すこともできます。しかし逐次型プログラミングではこのような書き方はできません。
まだ計算されていない未来の結果に対して処理を記述したいという要求は、処理1が複雑だったり、処理1の途中でエラーが発生する可能性があるような場合に出てきます。たとえば通信の入出力処理と、その結果を加工するような処理です。もし複雑な処理1とそれが終わった時の処理2を別々に記述することができるのであれば、見通しのよいプログラムを書くことができます。
このような要求はfutureと呼ばれるプログラミング パターンによって解決できます。Dartでは
Future<T>と
Completer<T>でこれを実現します。
サンプルコード
void main() {
final Completer<double> comp = new Completer<double>();
final Future<double> fut = comp.future;
fut.then((double a) {
// 処理2
double b = a * 2;
print("b=${b}");
});
// 処理1
double a = Math.random();
print("a=${a}");
comp.complete(a);
}
実行例
a=0.00013735424727201462
b=0.00027470849454402924
Completer<T>の型パラメーターTは、処理が終わったあとに受け取る値の型です。Completerのfutureプロパティで、そのCompleterで処理が終わったときに呼び出されるFutureのインスタンスが取得できます。このFutureインスタンスの型パラメーターもTになります。
処理が終わったらCompleterのcompleteメソッドを、処理結果とともに呼び出します。これによってFutureのthenメソッドに渡した関数が呼び出されます。
Futureを用いた実装では、処理2を処理1より前に書くことができています。このまま処理1を別の関数やクラスに持っていくこともでき、呼び出し側は処理2が終わったらその結果を受け取った時の処理だけを書くことができるようになるのです。
処理中のエラーを補足する
処理の途中で発生したエラー(例外)をFutureへ伝えるには、CompleterのcompleteExceptionを呼び出します。この例外はFutureのhandleExceptionメソッドで受け取ることができます。handleExceptionメソッドでtrueを返すと、そこで例外の処理が完了したことを通知します。
サンプルコード
void main() {
final Completer comp = new Completer();
final Future fut = comp.future;
fut.then((double a) {
double b = a * 2;
print("b=${b}");
});
fut.handleException((Object exception) {
print("exception=${exception}"); // exception=Error!
return true;
});
// 処理で発生したエラーをFutureに伝える。
comp.completeException("Error!");
}
複数のFutureの処理を続けて行う
2つのCompleterが存在し、それぞれ処理が終わったところでなにかの処理を順番に行わせるためには、Futureのchainメソッドを利用します。この機能を利用すると、複数のCompleterで処理を行わせ、その結果を順次処理できます。
chainメソッドの戻り値として次に呼び出されるべきFutureを返します。
void main() {
final Completer<int> comp1 = new Completer<int>();
final Completer<int> comp2 = new Completer<int>();
comp1.future.chain((int a) {
print("a=${a}");
return comp2.future;
}).then((int b) {
print("b=${b}");
});
// comp1とcomp2のcomplete順を入れ替えても結果は同じ。
// a=1
// b=2
comp1.complete(1);
comp2.complete(2);
}
結果を変換するFutureを作る
Futureのtransformメソッドを使うと、結果の値を変換して格納することができます。transformに変換処理を行う関数を渡して呼び出すと、その変換処理を伴うFutureが返ります。
void main() {
final Completer comp = new Completer();
// 単純なFuture
comp.future.then((int a) { print("a=${a}"); });
// 変換処理を伴うFuture
final Future tf = comp.future.transform((int a) => a * 2);
tf.then((int b) { print("b=${b}"); });
comp.complete(1);
}
実行結果
a=1
b=2
複数のFutureの結果を待つ
複数のFutureがすべて完了するのを待つためには
Futuresのwaitメソッドを使います。Futureのリストを渡すと、すべてのFutureの完了を待つ新しいFutureが返りますので、そのFutureインスタンスに対してthenメソッドを呼び出します。
サンプルコード
void main() {
final Completer<int> comp1 = new Completer<int>();
final Completer<int> comp2 = new Completer<int>();
// 複数のFutureがすべて完了するまで待つ。
final Future<List<int>> fs =
Futures.wait([ comp1.future, comp2.future ]);
fs.then((List<int> results) {
// 結果をすべて表示する。
for (int result in results) {
print("result=${result}");
}
});
// 完了
comp1.complete(1);
comp2.complete(2);
}
実行結果
result=1
result=2
Futureのプロパティ
Futureには結果や状態を知るためのプロパティが用意されていますので、これらの動作をよく理解することがFutureを使いこなすことにつながります。
なおここで処理中とは、completeあるいはcompleteExceptionメソッドが呼び出される前、正常終了後はcompleteメソッドが呼び出されたあと、エラー発生後はcompleteExceptionが呼び出されたあとのことです。
- isComplete
- 処理中:false
- 正常終了後:true
- エラー発生後:true
- hasValue
- 処理中:false
- 正常終了後:true
- エラー発生後:false
- value
- 処理中:FutureNotCompleteException例外がスローされる
- 正常終了後:結果の値
- エラー発生後:エラーの値(completeExceptionの引数)
- exception
- 処理中:FutureNotCompleteException例外がスローされる
- 正常終了後:null
- エラー発生後:エラーの値(completeExceptionの引数)