Subterranean Flower

DartのFutureを使って非同期処理を書く

Author
古都こと
ふよんとえんよぷよぐやま!!!

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を処理すれば、非同期処理の完了です。これで非同期なメソッドを実装することができました。