Dart 非同期プログラミング — Future / async-await

非同期プログラミングはプログラムを待たせない — 3 秒の I/O 操作で CPU はただ待つのではなく他の作業ができる。

1. 学べること


2. 開発者のリアルな物語

(1) 課題:シーケンシャルリクエストによる 12 秒のページロード

Bob の DataPipeline は 3 つの API からデータを取得する必要がある:orders(3 秒)、products(2 秒)、customers(2 秒)。彼は当初シーケンシャルリクエストを使った:3 + 2 + 2 = 7 秒。データベースクエリ(5 秒)を加えると、レポート生成全体で 12 秒かかった。ユーザー体験は悲惨で、SaaS 顧客から「遅すぎる」との苦情が寄せられた。

(2) 解決策:Future.wait による並行実行

Future.wait を使うと 3 つの API リクエストが並行実行され、合計時間は最も遅いもので決まる:3 秒。並行データベースクエリと組み合わせると、レポート生成全体が 4 秒に短縮される。

DART
// シーケンシャル:3 + 2 + 2 = 7 秒
final orders = await fetchOrders();     // 3秒
final products = await fetchProducts(); // 2秒
final customers = await fetchCustomers(); // 2秒

// 並行:max(3, 2, 2) = 3 秒
final results = await Future.wait([
  fetchOrders(),
  fetchProducts(),
  fetchCustomers(),
]);
TEXT
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。

(3) 効果


3. Future の基礎

(1) Future ライフサイクル

100%
sequenceDiagram
  participant Bob
  participant API1
  participant API2
  participant API3
  Bob->>API1: fetchOrders()
  Bob->>API2: fetchProducts()
  Bob->>API3: fetchCustomers()
  Note over Bob: Future.wait 並行
  API1-->>Bob: 120 万件の注文
  API2-->>Bob: 50 万件の商品
  API3-->>Bob: 30 万件の顧客
  Bob->>Bob: マージ&分析
TEXT
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。

▶ サンプル:Future の作成と使用

DART
import 'dart:async';

// 遅延後に完了する Future を作成
Future<String> fetchOrder() async {
  await Future.delayed(const Duration(seconds: 2));
  return 'ORD-001: \$1500.00 USD';
}

// エラーを伴う Future
Future<String> fetchOrderWithRetry() {
  return Future.delayed(const Duration(seconds: 1), () {
    throw Exception('Network timeout');
  });
}

void main() async {
  // Pending → Completed
  print('Fetching order...');
  final order = await fetchOrder();
  print(order);

  // Pending → Failed
  try {
    await fetchOrderWithRetry();
  } catch (e) {
    print('Error: $e');
  }
}
TEXT
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
Future 状態 意味
pending 未完了、待機中
completed with 値 値で成功完了
completed with エラー 失敗完了、例外を伴う

4. 非同期 / await 詳解

(1) 基本構文

▶ サンプル:非同期/await の基礎

DART
import 'dart:async';

Future<double> calculateOrderTotal(String orderId) async {
  // await は Future が完了するまで実行を一時停止
  final amount = await fetchAmount(orderId);
  final taxRate = await fetchTaxRate(orderId);
  return amount * (1 + taxRate);
}

Future<double> fetchAmount(String orderId) async {
  await Future.delayed(const Duration(milliseconds: 500));
  return 1500.0;
}

Future<double> fetchTaxRate(String orderId) async {
  await Future.delayed(const Duration(milliseconds: 300));
  return 0.08;
}

void main() async {
  final total = await calculateOrderTotal('ORD-001');
  print('Total: \$${total.toStringAsFixed(2)} USD');
}
TEXT
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。

(2) エラー処理

▶ サンプル:非同期/await エラー処理

DART
import 'dart:async';

Future<String> fetchApiData(String endpoint) async {
  await Future.delayed(const Duration(seconds: 1));
  if (endpoint.contains('invalid')) {
    throw Exception('API error: $endpoint not found');
  }
  return 'Data from $endpoint';
}

Future<void> robustFetch(String endpoint) async {
  try {
    final data = await fetchApiData(endpoint);
    print('Success: $data');
  } on Exception catch (e) {
    print('Exception: $e');
  } finally {
    print('Fetch completed for $endpoint');
  }
}

void main() async {
  await robustFetch('orders');           // Success
  await robustFetch('invalid-endpoint'); // Exception
}
TEXT
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
エラー処理方法 構文 ユースケース
try-catch try { await f(); } catch (e) {} 回復が必要な場合
catchError f().catchError((e) => ...) 関数型スタイル
onError f().then(..., onError: ...) シンプルなコールバック

5. Future 合成

(1) Future.wait — 並行待機

▶ サンプル:Future.wait 並行

DART
import 'dart:async';

Future<List<String>> fetchOrders() async {
  await Future.delayed(const Duration(seconds: 3));
  return ['ORD-001', 'ORD-002', 'ORD-003'];
}

Future<List<String>> fetchProducts() async {
  await Future.delayed(const Duration(seconds: 2));
  return ['Laptop', 'Mouse', 'Keyboard'];
}

Future<List<String>> fetchCustomers() async {
  await Future.delayed(const Duration(seconds: 2));
  return ['Alice', 'Bob', 'Charlie'];
}

void main() async {
  // 並行:max(3, 2, 2) = 約3秒
  final stopwatch = Stopwatch()..start();
  final results = await Future.wait([
    fetchOrders(),
    fetchProducts(),
    fetchCustomers(),
  ]);
  stopwatch.stop();

  print('Orders:   ${results[0]}');
  print('Products: ${results[1]}');
  print('Customers: ${results[2]}');
  print('Time: ${stopwatch.elapsedMilliseconds}ms');
}
TEXT
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。

▶ サンプル:Future.wait エラー処理

DART
import 'dart:async';

Future<String> riskyFetch(String name, bool shouldFail) async {
  await Future.delayed(const Duration(seconds: 1));
  if (shouldFail) throw Exception('$name failed');
  return '$name data';
}

void main() async {
  // Future.wait は早期失敗 - いずれかが失敗すると全体が失敗
  try {
    await Future.wait([
      riskyFetch('API-1', false),
      riskyFetch('API-2', true),   // これが失敗する
      riskyFetch('API-3', false),
    ]);
  } catch (e) {
    print('Future.wait failed: $e');
  }

  // eagerError: false で個別結果を保持
  final results = await Future.wait(
    [
      riskyFetch('API-1', false).then((v) => Result.success(v)),
      riskyFetch('API-2', true).then((v) => Result.success(v)).catchError((e) => Result.failure(e.toString())),
      riskyFetch('API-3', false).then((v) => Result.success(v)),
    ],
  );

  for (final r in results) {
    print(r.isSuccess ? 'OK: ${r.data}' : 'FAIL: ${r.error}');
  }
}

class Result<T> {
  final T? data;
  final String? error;
  final bool isSuccess;
  Result.success(this.data) : error = null, isSuccess = true;
  Result.failure(this.error) : data = null, isSuccess = false;
}
TEXT
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。

(2) Future.any — レース

▶ サンプル:Future.any レース

DART
import 'dart:async';

Future<String> fetchFromCache() async {
  await Future.delayed(const Duration(milliseconds: 100));
  return 'Cache: ORD-001 data';
}

Future<String> fetchFromApi() async {
  await Future.delayed(const Duration(seconds: 2));
  return 'API: ORD-001 data';
}

Future<String> fetchFromDb() async {
  await Future.delayed(const Duration(milliseconds: 500));
  return 'DB: ORD-001 data';
}

void main() async {
  // 最初に完了したものを返す
  final fastest = await Future.any([
    fetchFromCache(),
    fetchFromApi(),
    fetchFromDb(),
  ]);
  print('Fastest: $fastest');  // Cache: ORD-001 data
}
TEXT
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。

(3) Future.forEach — シーケンシャル反復

▶ サンプル:Future.forEach

DART
import 'dart:async';

Future<void> processOrder(String orderId) async {
  await Future.delayed(const Duration(milliseconds: 500));
  print('Processed: $orderId');
}

void main() async {
  final orders = ['ORD-001', 'ORD-002', 'ORD-003'];

  // シーケンシャル処理
  await Future.forEach(orders, processOrder);
  print('All orders processed');
}
TEXT
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
合成方法 動作 並行性 ユースケース
Future.wait すべてを待機 並行 バッチリクエスト
Future.any 最初に完了したもの 並行 レース / フォールバック
Future.forEach シーケンシャル完了 シーケンシャル 順序依存の操作

6. Completer 手動制御

▶ サンプル:Completer

DART
import 'dart:async';

// Completer による手動 Future 制御
class ApiService {
  final Completer<String> _initCompleter = Completer<String>();

  Future<String> get initialized => _initCompleter.future;

  void onConnected(String serverInfo) {
    if (!_initCompleter.isCompleted) {
      _initCompleter.complete(serverInfo);
    }
  }

  void onError(Object error) {
    if (!_initCompleter.isCompleted) {
      _initCompleter.completeError(error);
    }
  }
}

void main() async {
  final service = ApiService();

  // 非同期初期化をシミュレート
  Future.delayed(const Duration(seconds: 1), () {
    service.onConnected('Server v3.0.1, 1,000,000 records');
  });

  print('Waiting for initialization...');
  final info = await service.initialized;
  print('Connected: $info');
}
TEXT
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。

7. Bob のシナリオ:並行 API リクエスト

▶ サンプル:DataPipeline 並行データ取得

DART
import 'dart:async';

// API 呼び出しをシミュレート
Future<List<Map<String, dynamic>>> fetchOrders() async {
  await Future.delayed(const Duration(seconds: 2));
  return [
    {'id': 'ORD-001', 'amount': 1500.0, 'category': 'Electronics'},
    {'id': 'ORD-002', 'amount': 3200.0, 'category': 'Electronics'},
    {'id': 'ORD-003', 'amount': 890.0, 'category': 'Clothing'},
  ];
}

Future<Map<String, double>> fetchProductPrices() async {
  await Future.delayed(const Duration(seconds: 1));
  return {'Laptop': 1299.99, 'Mouse': 29.99, 'Keyboard': 79.99};
}

Future<List<String>> fetchCustomerNames() async {
  await Future.delayed(const Duration(seconds: 1));
  return ['Alice', 'Bob', 'Charlie'];
}

// エラー処理付きの並行データ取得
Future<void> generateReport() async {
  final stopwatch = Stopwatch()..start();

  try {
    // 全データを並行取得
    final results = await Future.wait([
      fetchOrders().then((v) => ('orders', v)).catchError((e) => ('orders', null)),
      fetchProductPrices().then((v) => ('products', v)).catchError((e) => ('products', null)),
      fetchCustomerNames().then((v) => ('customers', v)).catchError((e) => ('customers', null)),
    ]);

    stopwatch.stop();

    print('=== DataPipeline Report ===');
    print('Fetch time: ${stopwatch.elapsedMilliseconds}ms');

    for (final (key, value) in results) {
      if (value != null) {
        print('$key: OK (${value is List ? value.length : value.length} items)');
      } else {
        print('$key: FAILED');
      }
    }
  } catch (e) {
    print('Report generation failed: $e');
  }
}

void main() async {
  await generateReport();
}
TEXT
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。

8. 完全なサンプル:DataPipeline 非同期データ処理

DART
// ============================================
// DataPipeline 非同期データ処理
// Future、async/await、並行操作
// ============================================

import 'dart:async';

// シミュレートされたデータソース
Future<List<Map<String, dynamic>>> fetchOrders({int delay = 2}) async {
  await Future.delayed(Duration(seconds: delay));
  return [
    {'id': 'ORD-001', 'amount': 1500.0, 'status': 'completed', 'category': 'Electronics'},
    {'id': 'ORD-002', 'amount': 50.0, 'status': 'completed', 'category': 'Books'},
    {'id': 'ORD-003', 'amount': 3200.0, 'status': 'pending', 'category': 'Electronics'},
    {'id': 'ORD-004', 'amount': 890.0, 'status': 'completed', 'category': 'Clothing'},
  ];
}

Future<List<Map<String, dynamic>>> fetchProducts({int delay = 1}) async {
  await Future.delayed(Duration(seconds: delay));
  return [
    {'name': 'Laptop', 'price': 1299.99, 'category': 'Electronics'},
    {'name': 'Mouse', 'price': 29.99, 'category': 'Electronics'},
    {'name': 'Novel', 'price': 12.99, 'category': 'Books'},
  ];
}

Future<double> fetchTaxRate(String region) async {
  await Future.delayed(const Duration(milliseconds: 500));
  return switch (region) {
    'US' => 0.08,
    'EU' => 0.20,
    'UK' => 0.15,
    _ => 0.10,
  };
}

// 並行操作を含む非同期パイプライン
class AsyncPipeline {
  final String name;
  String status = 'idle';

  AsyncPipeline({required this.name});

  Future<Map<String, dynamic>> run({String region = 'US'}) async {
    status = 'running';
    final stopwatch = Stopwatch()..start();

    try {
      // ステップ 1: 並行データ取得
      final (orders, products, taxRate) = await (
        fetchOrders(),
        fetchProducts(),
        fetchTaxRate(region),
      ).wait;

      // ステップ 2: 注文を処理
      final completedOrders = orders
          .where((o) => o['status'] == 'completed')
          .toList();

      // ステップ 3: 売上を計算
      final revenue = completedOrders.fold<double>(
          0, (sum, o) => sum + (o['amount'] as double));

      final taxAmount = revenue * taxRate;
      final totalWithTax = revenue + taxAmount;

      // ステップ 4: カテゴリ別にグループ化
      final byCategory = <String, double>{};
      for (final order in completedOrders) {
        final cat = order['category'] as String;
        final amt = order['amount'] as double;
        byCategory.update(cat, (v) => v + amt, ifAbsent: () => amt);
      }

      stopwatch.stop();
      status = 'completed';

      return {
        'pipeline': name,
        'region': region,
        'taxRate': taxRate,
        'totalOrders': orders.length,
        'completedOrders': completedOrders.length,
        'revenue': revenue,
        'tax': taxAmount,
        'totalWithTax': totalWithTax,
        'byCategory': byCategory,
        'processingTime': stopwatch.elapsedMilliseconds,
        'status': status,
      };
    } catch (e) {
      status = 'failed';
      rethrow;
    }
  }
}

void main() async {
  final pipeline = AsyncPipeline(name: 'E-Commerce Analytics');

  print('=== Starting DataPipeline ===');
  final report = await pipeline.run(region: 'US');

  print('\n=== Report ===');
  print('Pipeline:    ${report['pipeline']}');
  print('Region:      ${report['region']}');
  print('Tax Rate:    ${(report['taxRate'] as double * 100).toStringAsFixed(1)}%');
  print('Total Orders: ${report['totalOrders']}');
  print('Completed:   ${report['completedOrders']}');
  print('Revenue:     \$${(report['revenue'] as double).toStringAsFixed(2)} USD');
  print('Tax:         \$${(report['tax'] as double).toStringAsFixed(2)} USD');
  print('Total:       \$${(report['totalWithTax'] as double).toStringAsFixed(2)} USD');
  print('Time:        ${report['processingTime']}ms');

  print('\nBy Category:');
  for (final entry in (report['byCategory'] as Map<String, double>).entries) {
    print('  ${entry.key}: \$${entry.value.toStringAsFixed(2)} USD');
  }
}
TEXT
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。

出力:

TEXT
=== Starting DataPipeline ===

=== Report ===
Pipeline:    E-Commerce Analytics
Region:      US
Tax Rate:    8.0%
Total Orders: 4
Completed:   3
Revenue:     $2440.00 USD
Tax:         $195.20 USD
Total:       $2635.20 USD
Time:        2000ms

By Category:
  Electronics: $1500.00 USD
  Books: $50.00 USD
  Clothing: $890.00 USD

❓ よくある質問

Q: 非同期 関数の戻り値型は何ですか? A: 非同期 関数は戻り値を自動的に Future にラップします。Future<T> 戻り型を宣言し、関数内では単純に T を返してください。

Q: await は 非同期 関数内でのみ使えますか? A: はい。await は 非同期 関数または 非同期* ジェネレーター内でのみ使用できます。Dart 3.5+ はトップレベル await をサポートします(Dart スクリプトのみ)。

Q: Future.wait で Future が失敗した場合はどうなりますか? A: デフォルトでは、いずれかの Future が失敗すると Future.wait は即座にその例外をスローします(他の Future は実行を続けますが、結果は無視されます)。eagerError: false を使うとすべてが完了するまで待機できます。

Q: 非同期/await と .then() の違いは何ですか? A: 機能的には同等ですが、非同期/await の方が読みやすく(線形フロー)、.then() の方が関数型的(チェーン)です。日常的な使用には 非同期/await を推奨します。.then() はシンプルな変換に適しています。

Q: いつ Completer を使うべきですか? A: Future の完了タイミングを手動で制御する必要があるときに使ってください。一般的なシナリオ:イベントコールバックから Future への変換、WebSocket メッセージ、サードパーティライブラリのコールバック。ほとんどの場合、非同期/await を使えば十分です。

Q: Future はマルチキャストですか、ユニキャストですか? A: Future はユニキャストです — 1 つの await または .then() でのみ消費できます。マルチキャストには Stream または asStream() が必要です。

Q: タイムアウト制御を実装するには? A: Future.timeout() メソッドを使います。例:await fetchApi().timeout(Duration(seconds: 5), onTimeout: () => defaultData)


📖 まとめ


📝 練習問題

  1. 基礎(難易度 ⭐):それぞれ 1/2/3 秒の遅延を持つ Future を返す 3 つの関数を書いてください。非同期/await を使ってシーケンシャルに呼び出し、合計時間を測定します。その後、Future.wait を使って並行に呼び出し、時間を比較します。
  2. 中級(難易度 ⭐⭐):リトライ付き非同期リクエスト関数を実装してください:fetchWithRetry(url, retries: 3, backoff: Duration)。失敗時に指数バックオフでリトライし、リトライ回数を超えた後に例外をスローします。
  3. 挑戦(難易度 ⭐⭐⭐):Completer を使ってシンプルな「リクエスト - レスポンス」プロトコルを実装してください:リクエストを送信し、対応する応答を待つ、タイムアウトサポート付き。複数のリクエストを並行送信できるメッセージバスをシミュレートします。

← 前のレッスン | 次のレッスン →

Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%