HTTP通信やI/O処理など、非同期処理というものは常に我々の身近にあります。Dartでは、そんな非同期処理を簡潔に扱うための仕組みとして、Futureというものが用意されています。この記事では、Futureの基本的な使い方について説明します。
なお、この記事で書かれている内容は2015年1月5日現在のものであり、将来的にサポートされる内容については触れていません。今後のDartでサポートされる予定のasync/await機能については[翻訳]Dartの非同期サポート:フェーズ1をご覧ください。
Futureを使う
Futureはdart:asyncライブラリに含まれています(※将来的にはdart:coreライブラリに移動させられる予定です)。Futureを利用する場合には、このライブラリをインポートする必要があります。
Futureを利用する非同期処理の代表的な例としては、ファイルのノンブロッキングI/O処理があります。dart:ioライブラリにはファイルを処理するためのAPIが揃っているので、例を見てみましょう。
import 'dart:io';
import 'dart:async';
main() {
var file = new File("myFile.txt");
Future<String> future = file.readAsString(); // このメソッドはFutureを返す
future.then((content) => print(content));
}
この例は、ファイル「myFile.txt」を読み込み、その内容を表示します。このサンプルを実行する際は、あらかじめ「myFile.txt」を作成しておき、何かしらの内容を書き込んでおいてください。
ここでreadAsString()は非同期メソッドなので、実際にファイルの内容を読み込み終わる前にコードは先へ進んでしまい、通常の方法では内容を表示することができません。
そこでFutureの出番になります。一般的に、非同期処理を行うメソッドは、処理が未来に完了することを表すFuture<T>オブジェクトを返します。この場合、readAsString()が、Future<String>を返しています。
返されたFuture<T>にthen(callback(T value))メソッドを使ってコールバック関数を登録しておくと、非同期処理が完了したときに、完了した値Tと共にコールバック関数が呼び出されます。この例の場合はFuture<String>となっており、読み込んだ内容がStringとしてコールバック関数に渡されます。
Futureのエラーハンドリング
非同期処理では例外の発生も非同期になるため、try-catch-finallyでは処理することができません。代わりにFutureに用意されているエラーハンドリングAPIを使用します。
import 'dart:io';
import 'dart:async';
main() {
var file = new File("myFile.txt");
Future<String> future = file.readAsString(); // このメソッドはFutureを返す
future.then((content) => print(content))
.catchError((error) => print(error))
.whenComplete(() => print("done."));
}
catchErrorはthenで発生した例外をキャッチし処理します。whenCompleteは例外が発生したかしていないかにかかわらず、最後に実行されます。これらは同期処理におけるtry-catch-finallyと同等であると考えてもらってもかまいません。
Futureのチェイン
thenメソッドはFutureを返すので、複数のFutureをチェインすることもできます。例えばファイルに内容を書き込み、それを読み込んで表示するコードを考えましょう。
import 'dart:io';
import 'dart:async';
main() {
var file = new File("myFile.txt");
Future<File> future = file.writeAsString("sample text.");
future.then((f) => file.readAsString())
.then((content) => print(content));
}
writeAsString(text)もreadAsStringもFutureを返します。この例では、writeAsStringから返ってきたFutureが完了したときに、さらにreadAsStringで非同期に内容を読み込み次のFutureを返し、それが完了すると内容を表示するという処理を行っています。
Futureをチェインする場合でも、エラーハンドリングは同様に行うことができます。
import 'dart:io';
import 'dart:async';
main() {
var file = new File("myFile.txt");
Future<File> future = file.writeAsString("sample text.");
future.then((f) => file.readAsString())
.catchError((e) => print(e))
.then((content) => print(content))
.catchError((e) => print(e));
}
Future.wait(list)
ここでひとつ便利なメソッドを紹介しておきましょう。複数の非同期処理を同時に走らせて、すべての処理が完了するのを待ちたいときがあります。そんなときはスタティックメソッドFuture.wait(list)が役に立ちます。
Future.wait(list)はFutureのリストを受け取り、すべての処理結果のリストを結果とするFuture<List>を発行します。これを利用することで複数のFutureをまとめて処理することができます。
import 'dart:io';
import 'dart:async';
main() {
var future1 = new File("myFile1.txt").readAsString();
var future2 = new File("myFile2.txt").readAsString();
var future3 = new File("myFile3.txt").readAsString();
// 全てのFutureをまとめる。
var futureAll = Future.wait([future1, future2, future3]);
futureAll.then((results) => print(results));
}
Futureを利用するメソッドを自作する
これまではFutureを利用する既存のメソッドのみについて取り扱いました。今度はFutureを使うメソッドを自作してみましょう。
Futureを使った処理を作るには、Completer<T>というクラスを利用します。
import 'dart:async';
main() {
someAsyncMethod().then((message) => print(message));
}
Future<String> someAsyncMethod() {
var completer = new Completer<String>(); // Completer<T>を作成する。
// 何かしら非同期な処理が完了したときに
// Completer<T>のcomplete(T value)メソッドを呼び出して処理を完了させる。
new Timer(new Duration(seconds: 1), () => completer.complete("done."));
return completer.future; // Completerの持つFutureオブジェクトを返す。
}
まずCompleterオブジェクトを作成します。そして非同期的な処理を行い、その中でCompleterのcomplete(T value)メソッドを呼び出すことで、対応するFutureの処理を完了させることができます。最後に、Completerの持つfutureオブジェクトを返り値として返します。あとは呼び出し側でFutureを処理すれば、非同期処理の完了です。これで非同期なメソッドを実装することができました。