Dart 3 新機能 — Records / パターンマッチング / シールクラス

Dart 3 の三大機能 — コードを「動く」から「エレガント」にアップグレードする。

1. 学べること


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

(1) 課題:深い型階層と冗長な if-else

Bob の DataPipeline には 3 つのデータソース(API/File/Database)があり、それぞれ設定と接続方法が異なる。彼は継承を使って実装したが、階層が深くコードが冗長だった。API レスポンスを解析する際、ステータスコードとレスポンス構造に対する多数の if-else チェックが必要で、たった 5 つのレスポンスタイプに対して 200 行の条件付きコードを書いた。

(2) Dart 3 新機能による解決

シールクラスはデータソース型を制限し、パターンマッチングはさまざまなレスポンスをエレガントに処理し、Records は軽量な複数の戻り値を提供する。

DART
// シールクラス: 網羅的なデータソース型
sealed class DataSource {}
class ApiSource extends DataSource { final String endpoint; ApiSource(this.endpoint); }
class FileSource extends DataSource { final String path; FileSource(this.path); }

// シールクラスのパターンマッチング - コンパイラが全ケース処理を保証
String describe(DataSource source) => switch (source) {
  ApiSource(:final endpoint) => 'API: $endpoint',
  FileSource(:final path) => 'File: $path',
};

// Records: 複数の戻り値
(String, double) parseResponse(String raw) => ('ORD-001', 1500.0);
TEXT
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。

(3) 効果


3. Records

(1) Record の基礎

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

DART
void main() {
  // 位置指定 Record
  var order = ('ORD-001', 1500.0, 'completed');
  print(order.$1);  // ORD-001
  print(order.$2);  // 1500.0
  print(order.$3);  // completed

  // 名前付き Record(可読性のため推奨)
  var order2 = (id: 'ORD-002', amount: 3200.0, status: 'pending');
  print(order2.id);      // ORD-002
  print(order2.amount);  // 3200.0
  print(order2.status);  // pending

  // 位置指定と名前付きの混在
  var mixed = ('ORD-003', 890.0, category: 'Electronics');
  print(mixed.$1);       // ORD-003
  print(mixed.category); // Electronics
}
TEXT
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。

▶ サンプル:関数戻り値としての Record

DART
// Record を使った複数の戻り値
({String id, double amount, double tax}) calculateOrder(double amount, double taxRate) {
  return (
    id: 'ORD-${DateTime.now().millisecondsSinceEpoch}',
    amount: amount,
    tax: amount * taxRate,
  );
}

// 簡易グループ化のための位置指定 Record
(String, double) parseAmount(String input) {
  final parts = input.split(':');
  return (parts[0], double.parse(parts[1]));
}

void main() {
  final order = calculateOrder(1500.0, 0.08);
  print('ID: ${order.id}, Amount: \$${order.amount}, Tax: \$${order.tax}');

  final (label, value) = parseAmount('Revenue:52500.75');
  print('$label: \$$value USD');
}
TEXT
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
Record 機能 構文 説明
位置指定フィールド .$1, .$2 位置でアクセス
名前付きフィールド .name 名前でアクセス
型アノテーション (String, int) または ({String name, int count}) 明示的な型
等価性 値の等価性 フィールド値が同じなら等しい

4. パターンマッチング

(1) パターンマッチングの種類

▶ サンプル:変数パターンと分解

DART
void main() {
  // 変数パターン - 値を抽出
  var (id, amount, status) = ('ORD-001', 1500.0, 'completed');
  print('ID: $id, Amount: $amount, Status: $status');

  // 名前付きフィールドでの Record 分解
  var (:id, :amount, :status) = (id: 'ORD-002', amount: 3200.0, status: 'pending');
  print('ID: $id, Amount: $amount, Status: $status');

  // List の分解
  var [first, second, ...rest] = [1, 2, 3, 4, 5];
  print('First: $first, Second: $second, Rest: $rest');

  // Map の分解
  var {'id': orderId, 'amount': orderAmount} = {'id': 'ORD-003', 'amount': 890.0};
  print('Order: $orderId, Amount: $orderAmount');
}
TEXT
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。

▶ サンプル:if-case パターン

DART
void main() {
  Object value = '1,500.00 USD';

  // if-case パターンマッチング
  if (value case String s when s.contains('USD')) {
    print('USD value: $s');
  }

  // Record に対するパターンマッチング
  var response = (statusCode: 200, body: '{"orders": 1500}');
  if (response case (statusCode: 200, :var body)) {
    print('Success response: $body');
  }

  // List に対するパターンマッチング
  var orders = ['ORD-001', 'ORD-002', 'ORD-003'];
  if (orders case [var first, var second, ...]) {
    print('First two: $first, $second');
  }
}
TEXT
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。

▶ サンプル:switch パターン

DART
String classifyOrder(Object value) => switch (value) {
  // 型パターン
  int i when i > 1000 => 'High value integer: $i',
  int i => 'Low value integer: $i',
  double d when d >= 1000 => 'Premium: \$${d.toStringAsFixed(2)}',
  double d => 'Standard: \$${d.toStringAsFixed(2)}',
  String s => 'String: $s',
  List l when l.length > 100 => 'Large batch: ${l.length} items',
  List l => 'Small batch: ${l.length} items',
  _ => 'Unknown type',
};

// Record でのパターンマッチング
String describeResponse((int, String) response) => switch (response) {
  (200, var body) => 'OK: $body',
  (404, _) => 'Not Found',
  (500, var msg) => 'Server Error: $msg',
  (>= 400, var msg) => 'Client Error: $msg',
  _ => 'Unknown response',
};

void main() {
  print(classifyOrder(1500));          // High value integer: 1500
  print(classifyOrder(890.0));         // Standard: $890.00
  print(classifyOrder([1, 2, 3]));     // Small batch: 3 items

  print(describeResponse((200, 'OK')));      // OK: OK
  print(describeResponse((404, 'Missing')));  // Not Found
  print(describeResponse((500, 'Crash')));    // Server Error: Crash
}
TEXT
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
パターンタイプ 構文 用途
変数パターン var x 値を抽出
型パターン Type x 型チェック + 抽出
定数パターン 42'hello' 完全一致
関係パターン >= 1000 範囲チェック
論理パターン a || ba && b 条件の組み合わせ
ガード when condition 追加条件
ワイルドカード _ 値を無視

5. シールクラス

(1) シールクラス階層

100%
classDiagram
  class DataSource {
    <<sealed>>
  }
  class ApiSource {
    +String endpoint
    +Map headers
  }
  class FileSource {
    +String path
    +Encoding encoding
  }
  class DatabaseSource {
    +String connectionString
  }
  DataSource <|-- ApiSource
  DataSource <|-- FileSource
  DataSource <|-- DatabaseSource
  note for DataSource "網羅的 switch 保証"
TEXT
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。

▶ サンプル:シールクラス定義と網羅的マッチング

DART
sealed class DataSource {
  const DataSource();
}

class ApiSource extends DataSource {
  final String endpoint;
  final Map<String, String> headers;

  const ApiSource(this.endpoint, {this.headers = const {}});
}

class FileSource extends DataSource {
  final String path;

  const FileSource(this.path);
}

class DatabaseSource extends DataSource {
  final String connectionString;

  const DatabaseSource(this.connectionString);
}

// 網羅的 switch - コンパイラが全サブタイプをチェック
String describeSource(DataSource source) => switch (source) {
  ApiSource(:final endpoint, :final headers) =>
      'API: $endpoint (${headers.length} headers)',
  FileSource(:final path) =>
      'File: $path',
  DatabaseSource(:final connectionString) =>
      'Database: $connectionString',
};

// 新しいサブタイプを追加して switch の更新を忘れた場合、
// コンパイラがエラーを報告する!
void main() {
  final sources = <DataSource>[
    ApiSource('https://api.example.com/orders', headers: {'Authorization': 'Bearer token'}),
    FileSource('/data/orders.csv'),
    DatabaseSource('postgresql://localhost:5432/orders'),
  ];

  for (final source in sources) {
    print(describeSource(source));
  }
}
TEXT
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
シールクラス機能 説明
サブクラス制限 同じライブラリ内で定義する必要がある
網羅性チェック switch が全サブタイプをカバーする必要がある
コンパイル時保証 新しいサブクラス追加時の switch 漏れはコンパイルエラーになる
インスタンス化不可 暗黙的に abstract

6. 三矢の活用

▶ サンプル:シール + パターン + Records の組み合わせ

DART
// API 結果用のシールクラス
sealed class ApiResult<T> {
  const ApiResult();
}

class Success<T> extends ApiResult<T> {
  final T data;
  final (int, String) metadata;  // Record: (statusCode, message)

  const Success(this.data, this.metadata);
}

class ApiError<T> extends ApiResult<T> {
  final String message;
  final int? statusCode;

  const ApiError(this.message, {this.statusCode});
}

class NetworkError<T> extends ApiResult<T> {
  final String reason;

  const NetworkError(this.reason);
}

// 分解付きのパターンマッチング
String handleResult(ApiResult<List<String>> result) => switch (result) {
  Success(:final data, metadata: (200, final msg)) =>
      'OK ($msg): ${data.length} items loaded',
  Success(:final data, metadata: (final code, _)) =>
      'Loaded with status $code: ${data.length} items',
  ApiError(:final message, statusCode: final code?) =>
      'API Error [$code]: $message',
  ApiError(:final message) =>
      'API Error: $message',
  NetworkError(:final reason) =>
      'Network Error: $reason',
};

void main() {
  final results = <ApiResult<List<String>>>[
    Success(['ORD-001', 'ORD-002'], (200, 'OK')),
    Success(['ORD-003'], (206, 'Partial Content')),
    ApiError('Rate limit exceeded', statusCode: 429),
    ApiError('Unknown error'),
    NetworkError('Connection timeout'),
  ];

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

7. Bob のシナリオ:DataPipeline のデータソースと結果解析

▶ サンプル:完全なデータソース抽象化

DART
sealed class DataSource {
  const DataSource();

  String get displayName;
}

class ApiSource extends DataSource {
  final String endpoint;
  final Duration timeout;

  const ApiSource(this.endpoint, {this.timeout = const Duration(seconds: 10)});

  @override
  String get displayName => 'API ($endpoint)';
}

class FileSource extends DataSource {
  final String path;
  final String format;

  const FileSource(this.path, {this.format = 'csv'});

  @override
  String get displayName => 'File ($path, $format)';
}

class DatabaseSource extends DataSource {
  final String connectionString;
  final String query;

  const DatabaseSource(this.connectionString, {this.query = 'SELECT * FROM orders'});

  @override
  String get displayName => 'Database ($connectionString)';
}

// Record を使った処理結果
typedef ProcessingResult = ({int processed, int skipped, double revenue, Duration time});

ProcessingResult processDataSource(DataSource source) => switch (source) {
  ApiSource(:final endpoint, :final timeout) => (
    processed: 50000,
    skipped: 120,
    revenue: 525000.0,
    time: timeout,
  ),
  FileSource(:final path, :final format) => (
    processed: 100000,
    skipped: 350,
    revenue: 1200000.0,
    time: Duration(seconds: 5),
  ),
  DatabaseSource(:final connectionString, :final query) => (
    processed: 1200000,
    skipped: 800,
    revenue: 15000000.0,
    time: Duration(seconds: 15),
  ),
};

void main() {
  final sources = <DataSource>[
    ApiSource('https://api.example.com/orders'),
    FileSource('/data/orders.csv'),
    DatabaseSource('postgresql://localhost:5432/orders'),
  ];

  print('=== DataPipeline Source Analysis ===');
  for (final source in sources) {
    final (:processed, :skipped, :revenue, :time) = processDataSource(source);
    print('\n${source.displayName}:');
    print('  Processed: ${processed} orders');
    print('  Skipped:   $skipped records');
    print('  Revenue:   \$${revenue.toStringAsFixed(2)} USD');
    print('  Time:      ${time.inSeconds}s');
  }
}
TEXT
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。

8. 完全なサンプル:DataPipeline レスポンス処理システム

DART
// ============================================
// DataPipeline レスポンス処理システム
// シールクラス + パターンマッチング + Records
// ============================================

// 異なるレスポンスタイプ用のシールクラス
sealed class ApiResponse {
  const ApiResponse();
}

class SuccessResponse extends ApiResponse {
  final int statusCode;
  final Map<String, dynamic> data;

  const SuccessResponse(this.statusCode, this.data);
}

class ErrorResponse extends ApiResponse {
  final int statusCode;
  final String message;
  final String? details;

  const ErrorResponse(this.statusCode, this.message, {this.details});
}

class TimeoutResponse extends ApiResponse {
  final Duration timeout;
  final String endpoint;

  const TimeoutResponse(this.timeout, this.endpoint);
}

class RedirectResponse extends ApiResponse {
  final String newEndpoint;
  final int statusCode;

  const RedirectResponse(this.newEndpoint, this.statusCode);
}

// Record としての処理結果
typedef Outcome = ({bool success, String message, double? revenue});

// 全レスポンスタイプに対するパターンマッチング - 網羅的!
Outcome handleResponse(ApiResponse response) => switch (response) {
  SuccessResponse(statusCode: 200, :final data) when data.containsKey('orders') => (
    success: true,
    message: 'Loaded ${data['orders']} orders',
    revenue: (data['revenue'] as num?)?.toDouble(),
  ),
  SuccessResponse(statusCode: 200, :final data) => (
    success: true,
    message: 'OK but no orders field: ${data.keys}',
    revenue: null,
  ),
  SuccessResponse(statusCode: 206, :final data) => (
    success: true,
    message: 'Partial data: ${data.length} fields',
    revenue: null,
  ),
  SuccessResponse(statusCode: final code) => (
    success: true,
    message: 'Unexpected success code: $code',
    revenue: null,
  ),
  ErrorResponse(statusCode: 429, :final message) => (
    success: false,
    message: 'Rate limited: $message',
    revenue: null,
  ),
  ErrorResponse(statusCode: final code, :final message, :final details?) => (
    success: false,
    message: 'Error [$code]: $message - $details',
    revenue: null,
  ),
  ErrorResponse(statusCode: final code, :final message) => (
    success: false,
    message: 'Error [$code]: $message',
    revenue: null,
  ),
  TimeoutResponse(:final timeout, :final endpoint) => (
    success: false,
    message: 'Timeout after ${timeout.inSeconds}s on $endpoint',
    revenue: null,
  ),
  RedirectResponse(:final newEndpoint, :final statusCode) => (
    success: false,
    message: 'Redirect ($statusCode) to $newEndpoint',
    revenue: null,
  ),
};

void main() {
  final responses = <ApiResponse>[
    SuccessResponse(200, {'orders': 50000, 'revenue': 525000.0}),
    SuccessResponse(200, {'products': 3000}),
    SuccessResponse(206, {'partial': true}),
    ErrorResponse(429, 'Rate limit exceeded'),
    ErrorResponse(500, 'Internal server error', details: 'Database connection lost'),
    ErrorResponse(404, 'Not found'),
    TimeoutResponse(const Duration(seconds: 30), '/api/v1/orders'),
    RedirectResponse('/api/v2/orders', 301),
  ];

  print('=== DataPipeline Response Handler ===\n');
  for (final response in responses) {
    final (:success, :message, :revenue) = handleResponse(response);
    final status = success ? 'OK' : 'FAIL';
    final rev = revenue != null ? ' (\$${revenue.toStringAsFixed(2)} USD)' : '';
    print('[$status] $message$rev');
  }
}
TEXT
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。

出力:

TEXT
=== DataPipeline Response Handler ===

[OK] Loaded 50000 orders ($525000.00 USD)
[OK] OK but no orders field: (products)
[OK] Partial data: 1 fields
[FAIL] Rate limited: Rate limit exceeded
[FAIL] Error [500]: Internal server error - Database connection lost
[FAIL] Error [404]: Not found
[FAIL] Timeout after 30s on /api/v1/orders
[FAIL] Redirect (301) to /api/v2/orders

❓ よくある質問

Q: Records と Classes の違いは何ですか? A: Records は匿名の値ベースの軽量データ構造で、Classes は名前があり、参照ベースで、メソッドを持てます。Records は単純なデータ渡しに適し、Classes は複雑なビジネスロジック用です。

Q: シールクラスと抽象クラスの違いは何ですか? A: シールクラスはサブクラスを同じライブラリに制限し、コンパイラが網羅性チェックを実行できるようにします。抽象クラスはどこでもサブクラスを定義でき、網羅性が妨げられます。網羅性保証が必要なときはシールを使ってください。

Q: パターンマッチングはどこで使えますか? A: switch 式、switch 文、if-case、変数宣言、for-in ループ。Dart 3 ではパターンはどこにでもあります。

Q: Records のパフォーマンスはどうですか? A: Records は通常のオブジェクトにコンパイルされ、パフォーマンスは小さなクラスと同等です。余分なオーバーヘッドはありませんが、クラスより速いわけでもありません。可読性に基づいて選んでください。

Q: シールクラスはコンストラクタを持てますか? A: ファクトリコンストラクタ(サブクラスを返す)は持てますが、クラス自体はインスタンス化できません(暗黙的に abstract)。コンストラクタは主にサブクラス間での初期化ロジックの共有に使われます。

Q: switch 式での _default の違いはありますか? A: 機能的には同じで、どちらも全キャッチマッチです。Dart 3 では簡潔さのため _(ワイルドカードパターン)の使用が推奨されます。default は互換性のために残されているレガシー構文です。

Q: Records を Map のキーに使えますか? A: はい。Records は値の等価性に基づき、==hashCode を自動実装します。(1, 2) == (1, 2) は true です。


📖 まとめ


📝 練習問題

  1. 基礎(難易度 ⭐):2 つの Record を定義してください:注文 ID と金額を表す位置指定 Record (String, double)、および ({String id, double amount, String status}) 名前付き Record。インスタンスを作成し、分解して出力してください。
  2. 中級(難易度 ⭐⭐):3 つの支払い方法(CreditCard/PayPal/BankTransfer)を定義するシールクラスを使用してください。それぞれ異なるフィールドを持ちます。switch 式を使って describePayment() メソッドを実装し、網羅性を保証します。
  3. 挑戦(難易度 ⭐⭐⭐):完全な API レスポンス処理システムを設計してください:レスポンスタイプ(Success/ValidationError/ServerError/Timeout)を定義するシールクラスを使用し、それぞれが Record データを運ぶようにします。関係パターンとガード条件を取り入れたパターンマッチングを使ってレスポンスハンドラを実装してください。

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

Web-Tutorial.com

Web-Tutorial 技術チーム

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

100%