Dartツアー(7)ライブラリと可視性、非同期サポート

技術,プログラミング言語

Flutterを始めてみましたが、コードはDart言語で書きます。という訳で、Dart言語のツアーを見て勉強しながら、自分用にメモをまとめてみました。今回はライブラリと可視性、非同期サポートです。

前回分は以下から。

ライブラリと可視性

  • importディレクティブとlibraryディレクティブは、モジュール化され共有可能なコードベースを作成する
  • ライブラリはAPIを提供するだけでなく、プライバシーの単位でもある
  • アンダースコア(_)で始まる識別子は、ライブラリ内部でのみ見ることができる
    • たとえ library ディレクティブを使用していなくても、すべての Dart アプリはライブラリ
  • ライブラリは、パッケージを使用して配布することができる

注:Dart が public や private などのアクセス修飾キーワードの代わりにアンダースコアを使用する理由を知りたければ、SDK issue 33383 を参照。

ライブラリの使用

  • あるライブラリの名前空間を別のライブラリのスコープで使用する方法を指定するには、importを使用する
    • Dart の Web アプリは一般に dart:html ライブラリを使用しインポートすることができる

ライブラリのプレフィックスを指定する

識別子が競合する2つのライブラリをインポートする場合、片方または両方のライブラリに接頭辞を指定することができる。たとえば、library1 と library2 の両方が Element クラスを持っている場合、次のようなコードになる。

import 'package:lib1/lib1.dart';
import 'package:lib2/lib2.dart' as lib2;

// Uses Element from lib1.
Element element1 = Element();

// Uses Element from lib2.
lib2.Element element2 = lib2.Element();

ライブラリの一部のみをインポートする

ライブラリの一部だけを使用したい場合は、ライブラリを選択的にインポートすることができる。例えば以下のようになる。

// Import only foo.
import 'package:lib1/lib1.dart' show foo;

// Import all names EXCEPT foo.
import 'package:lib2/lib2.dart' hide foo;

ライブラリの遅延ロード

遅延読み込み(レイジーローディングとも呼ばれる)とは、ウェブアプリが必要な時に必要な分だけライブラリを読み込むことを可能にする。以下は、遅延ロードを使用する場合の例です。

  • ウェブアプリの初期起動時間を短縮する
  • A/Bテストの実行 – たとえば、アルゴリズムの代替実装を試す
  • オプションの画面やダイアログなど、ほとんど使用されない機能をロードする場合

注:dart2jsのみディファードローディングに対応している。Flutter、Dart VM、および dartdevc は遅延ロードをサポートしていない。詳しくは、 issue #33118issue #27776 を参照。

ライブラリを遅延ロードするためには、まず deferred as を使ってライブラリをインポートする必要がある。

import 'package:greetings/hello.dart' deferred as hello;

ライブラリが必要になったら、ライブラリの識別子を使ってloadLibrary()を呼び出す。

Future<void> greet() async {
  await hello.loadLibrary();
  hello.printGreeting();
}

前のコードでは、await キーワードはライブラリがロードされるまで実行を一時停止する。asyncとawaitの詳細については、非同期サポートを参照。

一つのライブラリに対してloadLibrary()を複数回起動しても問題ない。ライブラリは一度だけロードされる。

遅延読み込みを使用する場合は、以下の点に注意する。

  • 遅延ライブラリの定数は、インポートファイルの定数ではない。これらの定数は、遅延ライブラリがロードされるまで存在しないことを忘れない
  • 遅延ライブラリの型は、インポートファイルでは使用できない。代わりに、遅延ライブラリとインポートファイルの両方によってインポートされるライブラリにインターフェース型を移動することを検討する
  • Dartは、deferred as namespaceを使用して定義した名前空間にloadLibrary()を暗黙のうちに挿入する。loadLibrary()関数はFutureを返す

ライブラリの実装

ライブラリパッケージの作成」には、どのようにライブラリパッケージを実装するかについて、以下のアドバイスがある。

  • ライブラリのソースコードの整理方法
  • export指示文の使用方法
  • part ディレクティブを使用するタイミング
  • library ディレクティブを使用するタイミング
  • 条件付き import と export を使って、複数のプラットフォームをサポートするライブラリを実装する方法

非同期サポート

Dartのライブラリには、FutureやStreamオブジェクトを返す関数がたくさんある。これらの関数は非同期である。つまり、時間のかかる操作(I/Oなど)を設定した後に、その操作が完了するのを待たずに返される。

asyncとawaitキーワードは非同期プログラミングをサポートし、同期コードと同じように見える非同期コードを書くことができるようになる。

Futureの取り扱い

完了したFutureの結果が必要なとき、2つの選択肢がある。

asyncとawaitを使ったコードは非同期だが、見た目は同期のコードによく似ている。例えば、以下は非同期関数の結果を待つためにawaitを使用したコードです。

Future<void> checkVersion() async {
  var version = await lookUpVersion();
  // Do something with version
}

awaitを使用するには、非同期関数(asyncとマークされた関数)内にコードを記述する必要がある。

注:非同期関数は時間のかかる処理を行うかもしれないが、その処理を待つことはない。その代わり、非同期関数は最初のawait式に遭遇するまでしか実行しない(詳細)。そして、Futureオブジェクトを返し、await式が完了した後に実行を再開する。

try、catch、finally を使用して、await を使用するコードでエラーとクリーンアップを処理する。

try {
  version = await lookUpVersion();
} catch (e) {
  // React to inability to look up the version
}

非同期関数の中でawaitを複数回使用することができる。例えば、以下のコードでは関数の結果を3回待つ。

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

await式では、expressionの値は通常Futureであり、そうでない場合は自動的にFutureでラップされる。このFutureオブジェクトは、あるオブジェクトを返すという約束を示す。await式の値は、その返されたオブジェクトです。await式は、そのオブジェクトが利用できるようになるまで実行を一時停止させる。

awaitを使用する際にコンパイルエラーが発生する場合は、awaitが非同期関数内にあることを確認する。たとえば、アプリの main() 関数で await を使用するには、main() の本体を非同期としてマークする必要がある。

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

注:この例では、結果を待たずに非同期関数 (checkVersion()) を使用しているが、これは、関数が実行を終了したと仮定した場合に問題を引き起こす可能性がある。この問題を避けるには、unawaited_futures リンター・ルールを使用する。

futures、async、awaitの使い方をインタラクティブに紹介する、非同期プログラミングのコードラボをご覧ください。

async関数の宣言

非同期関数は、ボディに async 修飾子を持つ関数である。

関数にasyncキーワードを追加すると、Futureを返すようになる。例えば、この同期関数がStringを返すとすると、以下のようになる。

String lookUpVersion() => '1.0.0';

これを非同期関数に変更した場合、例えば、Futureの実装には時間がかかるため、戻り値はFutureとなる。

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

関数の本体がFuture APIを使う必要はないことに注意する。Dartは必要に応じてFutureオブジェクトを作成する。関数が有用な値を返さない場合は、その戻り値の型を Future にする。

futures、async、awaitの使い方をインタラクティブに紹介する、非同期プログラミングのコードラボをご覧ください。

Streamの取り扱い

Streamから値を取得する必要があるとき、2つのオプションがある。

  • 非同期と非同期forループ(await for)を使用する
  • ライブラリツアーで説明されているように、Stream APIを使用する

注:await for を使う前に、それがコードをより明快にしストリームのすべての結果を本当に待ちたいのかどうかを確認すること。たとえば、UI のイベントリスナーには await for を使わないようにする。

非同期のforループは次のような形式である。

await for (varOrType identifier in expression) {
  // Executes each time the stream emits a value.
}

expression の値は Stream 型でなければならない。実行は以下のように行われる。

  1. ストリームが値を出力するまで待つ
  2. forループの本体を実行し、変数にその値が設定される
  3. ストリームが閉じるまで、1,2を繰り返す

ストリームのリッスンを停止するには、break文またはreturn文を使い、forループから抜け出しストリームからの購読を停止する。

非同期のforループを実装する際にコンパイルエラーが発生する場合は、await forが非同期関数の中にあることを確認すること。たとえば、アプリの main() 関数で非同期 for ループを使用するには、 main() の本体を非同期とマークする必要がある。

void main() async {
  // ...
  await for (final request in requestServer) {
    handleRequest(request);
  }
  // ...
}

一般的な非同期プログラミングの詳細については、ライブラリツアーのdart:asyncセクションを参照。

サンプル

上記のツアーの文章だけでは今ひとつわからないので、参照しろと言っている非同期プログラミングのコードラボからサンプルを引っ張ってきました。

12行目からのmain関数では2つの関数を呼び出しています。1個目はcountSecond()で、1秒毎に1から数字をカウントアップします。mainはasyncキーワードがついています。実行はcountSecond()が呼び出されてから1秒後からなので、main関数の処理はすぐに次のprintOrderMessage()に移ります。

printOrderMessage()では処理の前後にprint文でメッセージを出しています。これはすぐに実行されるので、このプログラムを実行するとまず’Awating user order…’の一文が表示されます。次に3行目の処理に移りますが、await付きなのでfetchUserOrder()の結果を待ちます。

fetchUserOrder()では’Large Latte’という文字列を返しますが、4秒後になります。その間も、mainから呼ばれたcountSeconds()の処理が進行しているので、1,2,3…と1秒毎に表示されていきます。

fetchUserOrder()から処理が戻るとprintOrderMessage()の後ろのprint文で’Your order is: …’の部分にfetchUserOrder()から返ってきた文字列を埋め込んで表示して、main関数に戻ります。

なお、1行目や12行目のasyncキーワードを付けないとエラーになります。また、3行目のawaitを取ってしまうと、処理を待たずに進んでしまうので、2行目のメッセージに続けて4行目のメッセージが出力されてしまうし、$orderの中身も"Instance of '_Future<String>'"になってしまいます。


次回

次回はツアーの残り部分になります。

2022-04-07技術Dart,プログラミング言語

Posted by woinary