これまでのDartの常識では、複数の同時並行の非同期処理ではFuture.wait
を使用するというものでした。しかしながら、現在はもっと型安全に実装する手段があります。
まず、従来のケースを見てみましょう。
class SomeResponse1 {
final String name;
const SomeResponse1({required this.name});
}
class SomeResponse2 {
final String code;
const SomeResponse2({required this.code});
}
Future<SomeResponse1> fetchSomeResponse1() async =>
const SomeResponse1(name: "test");
Future<SomeResponse2> fetchSomeResponse2() async =>
const SomeResponse2(code: "test2");
Future<void> main() async {
final [result1, result2] =
await Future.wait([fetchSomeResponse1(), fetchSomeResponse2()]);
print((result1 as SomeResponse1).name);
print((result2 as SomeResponse2).code);
}
上記の例では、Future.waitを使用して、2つの非同期処理が完了したらその結果を表示するというサンプルです。
このコードには、潜在的な下記のような危険があります。
- result1, result2をキャストしないと値にアクセスできません。つまり、キャストする型を間違えると実行時エラーになります。ビルド時に気づくことができません。
- 結果の数を誤ってもビルドが通ります。実行時に
Pattern matching error
が発生します。
Dart 3.0以降、Recordクラスが導入されましたが、これに合わせてdart:async
に便利なユーティリティ拡張が実装されています。 FutureRecord2.waitです。これを用いると、安全に複数の非同期処理をそれぞれ異なる型で結果を適切に受け取ることできます。
これを用いてmain()関数を書き直してみます。
Future<void> main() async {
print("test");
final (result1, result2) =
await (fetchSomeResponse1(), fetchSomeResponse2()).wait;
print(result1.name);
print(result2.code);
}
キャストする必要がなくなりました。 また、Recordの型が合わなくなるため、2つの個数が誤っていても実行時ではなく、Analyzerの解析の時点でエラーが出るようになりました。
これは、FutureRecord9まであるので、9個までであればこの方法で同時に非同期処理を、安全に流すことができます。
エラーハンドリング
class SomeResponse1Exception implements Exception {}
class SomeResponse2Exception implements Exception {}
Future<SomeResponse1> fetchSomeResponse1() async =>
throw SomeResponse1Exception();
Future<SomeResponse2> fetchSomeResponse2() async =>
throw SomeResponse2Exception();
Future<void> main() async {
try {
final [result1, result2] =
await Future.wait([fetchSomeResponse1(), fetchSomeResponse2()]);
print((result1 as SomeResponse1).name);
print((result2 as SomeResponse2).code);
} on SomeResponse1Exception catch (e) {
print(e);
} on SomeResponse2Exception catch (e) {
print(e);
}
}
ここで、レスポンスがエラーを返したとしましょう。この場合も、後述の.wait
を使用するメリットがあります。
たとえば、上記で両方例外が発生した場合、DartPadでは下記のような結果が得られます。
Instance of 'SomeResponse1Exception'
fetchSomeResponse1
の例外は処理できていますが、fetchSomeResponse2
は欠落していることがわかります。
ところが、この問題もFuture<T1, T2> wait
は解決します。
try {
final (result1, result2) =
await (fetchSomeResponse1(), fetchSomeResponse2()).wait;
print(result1.name);
print(result2.code);
} on ParallelWaitError<(SomeResponse1?, SomeResponse2?),
(AsyncError?, AsyncError?)> catch (e) {
print(e.errors.$1);
print(e.errors.$2);
print(e.values.$1);
print(e.values.$2);
}
型を明示的に宣言するか、dynamicにする必要があるものの、途中のどれかが失敗しても、個別に例外と結果を受け取ることができます。
そういうわけで、Future.waitを使用するよりFuture.waitを使用したほうが大抵のケースではメリットのほうが大きいというお話でした。
Future.waitはいらない子か
Future<(T1, T2, T3, T4, T5, T6, T7, T8, T9)> wait
という力技からもわかるように、Record型では限界があります。あるいは、
Future<int> sleepedPrint(int delay) async {
await Future.delayed(Duration(seconds: delay));
print("${delay} seconds waited.");
return delay * delay;
}
Future<void> main() async {
final result =
await Future.wait([for (var i = 1; i < 30; i++) sleepedPrint(i)]);
print(result);
}
上記のような例はRecord型では表現できません。型もすべて同じ型(int)を返すので、resultはList<int>
に解決できます。必ずしもRecord型のwaitがいいわけではありませんが、大抵のユースケースでは便利だと考えられます。