前のページ

言語編

次のページ


非同期対応 (Asynchrony support)



Dartでは、ブラウザのユーザ・インターフェイスdart:htmlなどでイベント処理の為の豊富なAPIが用意されている。加えてdart:async(エイシンクと発音)ライブラリにFutureCompleter及びStreamクラスが用意されている。非同期関数はある時間のかかり得る操作(例えばI/O)の設定が終わった後で戻り、その操作が完了するのを待たない。下表に示すとおり非同期関数はFutureまたはStreamのオブジェクトを返す。Futureはあるイベントを非同期処理するためのコンセプトであり、StreamIterableを非同期化する--つまりイベントの連続を扱うためのコンセプトである。これらの詳細を以下の節で記述することにする。

関数の4つの型


単一

複数

同期

T

Iterable<T>

非同期

Future<T>

Stream<T>


Dart言語仕様担当のGilad Bracha氏は201410月に新規の機能として非同期関数を解説している。これは:

  • await

  • asyncメソッド

であり、これにより非同期処理を扱うコードが同期コードと同じような記述となり、大幅にコーディングを簡素化できるようなった。またこの2つのメソッドを使うことで、FutureまたはStreamAPIを直接使う必要性が少なくなっている

バージョンに関する注意:Dart 2.1時点ではFutureStreamAPIを使うためにdart:asyncをインポートする必要がなくなっている。何故ならdart:coreがこれらのクラスをエクスポートしているからである。



非同期関数 (async functions)

非同期関数 (async functions)とはそのボディ部にasync修飾子が付された関数のことをいう。

foo() async => 42;

非同期関数が呼ばれるとその関数は直ちにFutureを返す。非同期関数のボディ部の実行開始は非同期処理のスケジューラが行う。ボディ部の実行が終了したら、その結果が正常だったかあるいは例外が発生したかにかかわらずそのFutureは完了する。この例ではfooは呼ばれたら直ちにFutureを返し、そのFutureは最終的には42という数字で完了する。

これはasync修飾子を使わなくても次のように記述できる:

foo() => new Future(() => 42);

しかしasync修飾子でコードは多少は簡素化にはなるが、一番のポイントは関数のなかでawait式が使えるようになることである。



Await(Await expressions)

await式を使うと非同期のコードをあたかも同期コードであるかのごとき記述が可能になる。例えばmyFileというFile(詳細はdart:ioFileクラスを参照のこと)のオブジェクトとした変数を考えてみよう。このファイルを新しい場所であるnewPathにコピーしたければ、まずそのファイルパスを宣言する:

String newPath = '/some/where/out/there';

そうすると次の行でコピーができ、trueが得られそうである:

myFile.copy(newPath).path == newPath;

しかしながらDartI/Oライブラリは非同期なので、copy操作はFutureを返すだけであり、それにはpathを呼ぶことはできない。従ってcopy()から返されたFutureに対するコールバック関数を用意しなければならない。そのコールバック関数が到来パラメタfで比較を行うことになる:

myFile.copy(newPath).then((f) => f.path == newPath);

これは七面倒くさい記述である。単に非同期のファイル・コピー操作が終了するのを待ってその結果を得たら実行を再開するだけである。await式を使うとその式のとおり事柄が進むことになる:

(await myFile.copy(newPath)).path == newPath;

このawait式が走るとmyFile.copy()が呼び出され、Futureが返される。実行はそこで止まりそのFutureが完了するのを待機する。そのFuturefileで完了したら実行が再開される。await式の値はそのFutureの完了値で、即ち待っていたファイルである。そうすれば属性値pathをとりだせ、newPathとそれとを比較できるようになる。これは確かに記述がすっきりするので、重宝しそうである。

一般的にはawait式は次の書式をとる:

await e

ここでeは単項式である。一般的にeは非同期処理であり、その値はFutureとなろう。このawait式はeを計算し、次にその結果が「良し」となる(即ちそのFutureが完了する)まで現在の処理を停止する。このawait式の結果はこのFutureの完了である。もしこのFutureが値ではなくてエラーで完了したときは、このawait式はこの実行が再開された時点でそのエラーをスローする。これは非同期コードにおける例外処理を大きく簡素化する。もしeFutureを返さなかったらどうなるか?この場合はひたすら待つだけである。

await式は非同期関数の中でのみ使用可能である。通常の関数の中で使おうとしたらコンパイル・エラーとなる。



Futureの取り扱い (Handling Futures)

Futureというのはその名前のとおり将来いつの時点化に結果を渡すという約束に似ている--promiseという呼び方をしている言語もある。完了したFutureの結果が必要な時は2つの選択肢がある:

asyncawaitを使っているコードは非同期であるが、これは同期のコードと類似している。例えばある非同期関数の結果を待つのにawaitを使っている幾つかのコードを示す:

await lookUpVersion();

awaitを使うにはそのコードはasync関数(asyncとマークされた非同期関数)内に置かれねばならない:

Future checkVersion() async {
  var version = await lookUpVersion();
  // 検索が終わったversionで何かを実行
}

注記:async関数は時間がかかる操作たちを実行するかもしれないが、この関数はそれらの操作が終わるのを待たない。そうではなくて、そのasync関数はその最初のawait式に出くわすまでの操作のみを実行する(詳細)。次にこの関数はFutureオブジェクトを返し、このawait式が完了したのちにのみ実行を再開する。

awaitを使っているコードの中でエラーとクリーンアップに対処するためにはtrycatch、及びfinallyを使用する:

try {
  version = await lookUpVersion();
} catch (e) {
  // このversionが検索できないことへの対応
}

async関数内でawaitを複数回使うことは可能である。例えば、以下のコードでは関数の結果に対し3回待っている:

var entrypoint = await findEntrypoint();
var exitCode = await runExecutable(entrypoint, args);
await flushThenExit(exitCode);

await式の中では、式の値は通常Futureである;そうなっていない場合はその値は自動的にFutureの中に包み込まれる。このFutureオブジェクトはあるオブジェクトを返す約束であることを示す。await式の値は返されたオブジェクトである。await式はそのオブジェクトが得られるまで実行を保留する。

awaitを使う際コンパイル時エラーが起きたら、awaitasync関数内にあることを確認されたい。例えば、awaitmain()関数内で使う場合は、main()のボディはasyncとマークされていなければならない:

Future main() async {
  checkVersion();
  print('In main: version is ${await lookUpVersion()}');
}



async関数の宣言 (Declaring async functions)

async関数はそのボディがasync修飾子でマークされている関数である。

ある関数にasyncキーワード付すとその関数はFutureを返すようになる。例えば、Stringを返すこの同期関数を考えてみよう:

String lookUpVersion() => '1.0.0';

この関数を非同期関数に変えたら—例えば将来の実装で時間がかかるものになりそうだから--返される値はFutureである:

Future<String> lookUpVersion() async => '1.0.0';

この関数のボディはFutureAPIを使う必要はないことに注意。Dartは必要に応じそのFutureを生成する。

もし自分の関数が有用な値を返さない場合は、その関数の戻りの型をFuture<void>とする。



ストリームを扱う (Handling Streams)

Stream(イベントが連続して発生するもの)からの値たちを取得する場合は2つのオプションがある:

  • asyncと非同期forループ(await for)を使う

  • library tourにあるようにStreamAPIを使う

注意:await forを使う前に、それでコードがよりすっきりするか、またそのストリームの結果の総てを本当に待ちたいのかを確認すべきである。例えば、UIのフレームワークはイベントの終わりのないストリームを送ってくるので、UIイベント・リスナの為にはawaitを使うべきではない。例えばブラウザに表示したボタンがクリックされるイベント(これもストリーム)を待つにはAPIonClickを使えば以下のように書ける(但しこのままだと何時までもクリックを待つ続けるので処置が必要だが:

document.querySelector("#button").onClick.listen((result) => print('Accepted $result'));

このコードはボタンがクリックされるたびに非同期でそれをコンソールにプリントする。

非同期のforループは以下の形式をとる:

await for (varOrType identifier in expression) {
  // このストリームが値を放出してくる度に実行する
}

式の値はStreamの型を有していなければならない。実行のプロセスは以下のようになる:

  1. このストリームが値を放出してくるのを待つ

  2. その放出されたきた値をこの変数にセットしてこのforループのボディを実行する

  3. このストリームが閉じられるまで12を繰り返す

このストリームをリスンするのを停止するには、breakまたはreturn文が使え、これはforループを解除し、このストリームへのリスンを解除する。

awaitを使う際コンパイル時エラーが起きたら、awaitasync関数内にあることを確認されたい。例えば、awaitmain()関数内で使う場合は、main()のボディはasyncとマークされていなければならない:

Future main() async {
  // ...
  await for (var request in requestServer) {
    handleRequest(request);
  }
  // ...}

非同期プログラミングに関する更なる一般的情報は次章以降を参照のこと。また Dart Language Asynchrony Support: Phase 1Dart Language Asynchrony Support: Phase 2の記事、及びDart言語仕様書を参照されたい。またFuture(及びCompleter)の使い方についてはGoogleSeth Laddのブログなども参考になる。





前のページ

次のページ