mcpField
D
dart-use-pattern-matching

dart-use-pattern-matching

github.com/flutter開発3.1k14.8k 回インストール2 日前に更新
リポジトリ

dart-use-pattern-matching について

JSON、パス、レコード、sealed クラスなどを扱う Dart 開発者向けに、パターンと switch の選択・使用方法を示すスキルです。網羅性の確認、早期エラー、過剰な構文の回避も扱います。

主な機能

  1. 01データ構造に応じたパターン選択
  2. 02JSON とパス要素の分解
  3. 03switch 式による多態データ処理
  4. 04網羅性確認とアンチパターン回避

活用シーン

  1. 01JSON フィールドの検証と抽出
  2. 02URL やパス要素の解析
  3. 03sealed クラスの網羅的な処理

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/flutter/agent-plugins --skill dart-use-pattern-matching

インストール後、Claude Code で/dart-use-pattern-matchingを入力して読み込まれたか確認します。

同梱ファイルexamples/json_patterns.dart

詳細

Dart パターンの実装

目次

パターン選択戦略

データ構造と目的に応じて、適切なパターンの種類を適用します。以下の条件に関するガイドラインに従ってください。

  • デシリアライズされたデータ(例:JSON)の検証と抽出を行う場合: Map、List、Object パターンを使用して、スキーマ構造の検証とプロパティの分解を一度に行います。
  • URL/パスのセグメント(uri.pathSegments、p.split(path))や String.split() のトークンを調べる場合: 手動の .length チェック、.first、.skip(1)、length - N によるインデックス計算の代わりに、レスト要素を含む List パターン(['api', 'comments', ...]、['assets', ...final rest]、[..., final parent, _])を使用します。
  • 多相のペイロードやレスポンスを処理する場合: マップの判別キーに対する switch 式を使用し、sealed クラス階層にデシリアライズします。
  • 複数の戻り値を処理する場合: Record パターンを使用して、フィールドをローカル変数に直接分解します。
  • オブジェクトまたは MapEntry から複数のフィールドを展開する場合: nullable プロパティを昇格する場合や、複数のステートメントで使う3個以上のフィールドを展開する場合は、単一呼び出しの受け渡しではなく、Object パターン(final Spec(:field1, :field2!) = spec;、for (final MapEntry(:key, :value) in map.entries))を使用します。
  • 型ごとの動作(代数的データ型)を実行する場合: sealed クラスと Object パターンを組み合わせて、網羅性を確保します。
  • 数値の範囲や条件を照合する場合: switch の各分岐内で、関係演算子(>=、<=)と論理積(&&)のパターンを使用します。
  • 複数のケースでロジックを共有する場合: 論理和(||)のパターンを使用して、単一のケース本体またはガード節を共有します。
  • 特定の値を無視する場合: ワイルドカードパターン(_)またはコレクション内の不一致となるレスト要素(...)を使用します。

Switch ステートメントと式

実行コンテキストに応じて、適切な switch 構文を選択します。

  • 値を生成する場合: switch 式を使用します。
    • 構文:switch (value) { pattern => expression, }
    • ルール:各ケースは単一の式でなければなりません。暗黙のフォールスルーはありません。網羅的でなければなりません。
  • ステートメントや副作用を実行する場合: switch ステートメントを使用します。
    • 構文:switch (value) { case pattern: statements; }
    • ルール:空のケースは次のケースへフォールスルーします。空でないケースは暗黙的に break します(break キーワードは不要です)。

コアパターンの実装

以下の構文とルールを使用してパターンを実装します。

  • 論理和(||): pattern1 || pattern2。両方の分岐で、まったく同じ変数の集合を定義する必要があります。
  • 論理積(&&): pattern1 && pattern2。各分岐で、重複する変数を定義してはいけません。
  • 関係演算子: ==、!=、<、>、<=、>= の後に定数式を指定します。
  • キャスト(as): pattern as Type。値が型に一致しない場合は例外がスローされます。分割代入で型を強制的にアサートする場合に使用します。
  • null チェック(?): pattern?。値が null の場合、パターンマッチは失敗します。変数は null 非許容の基底型に束縛されます。
  • null アサート(!): pattern!。値が null の場合は例外がスローされます。
  • 変数: var name または Type name。一致した値を新しいローカル変数に束縛します。
  • ワイルドカード(_): 任意の値に一致し、その値を破棄します。
  • リスト: [pattern1, pattern2]。Rest 要素(... または ...var rest)を使用しない限り、長さが正確に一致するリストにマッチします。
  • マップ: {"key": pattern}。指定したキーを含むマップにマッチします。一致しないキーは無視されます。
  • レコード: (pattern1, named: pattern2)。形状が正確に一致するレコードにマッチします。ゲッター名を推論するには :var name を使用します。
  • オブジェクト: ClassName(field: pattern)。ClassName のインスタンスにマッチします。ゲッター名を推論するには :var field を使用します。

実用上のバランスとアンチパターン

パターンマッチングと switch 式は、コードを簡潔にするために使用し、構文上の負担を増やさないようにしてください。次の境界を守ってください。

1. 単一の型昇格可能な変数には if-case より is による型昇格を優先する

単一の変数をチェックまたは昇格する場合は、別名のシャドウ変数を導入する if-case パターンではなく、標準の is チェックを使用します。

  • 推奨:
    // ✅ 余分な変数を使わず、`key` をその場で直接昇格します
    for (final MapEntry(:key, :value) in map.entries) {
      if (key is String && value != null) {
        process(key, value);
      }
    }
    
  • 非推奨:
    // ❌ アンチパターン:不要な別名変数 `k` を導入します
    for (final MapEntry(:key, :value) in map.entries) {
      if (key case final String k when value != null) {
        process(k, value);
      }
    }
    
2. switch アームでは nullable 型をまとめる

null と型 T の両方が有効で、同じように処理される値をマッピングまたは返す場合は、冗長な null アームを作成せず、nullable 型 T? に直接マッチさせます。

  • 推奨:
    // ✅ 簡潔な nullable パターンマッチ
    switch (value) {
      final String? s => s,
      _ => throw FormatException('Invalid value: $value'),
    }
    
  • 非推奨:
    // ❌ null のアームを個別に設けており冗長です
    switch (value) {
      final String s => s,
      null => null,
      _ => throw FormatException('Invalid value: $value'),
    }
    
3. 早期失敗の検証を維持する(データを黙って破棄しない)

不正なデータがエラーや診断警告を引き起こすべき場合、ループやデシリアライズで要素をフィルタリングするために if-case を使用しないでください。

  • 推奨:
    // ✅ 明示的な診断エラーで早期に失敗させます
    for (final raw in rawTasks) {
      if (raw is! Map<String, dynamic>) {
        throw FormatException('Expected Map item, got ${raw.runtimeType}: $raw');
      }
      _applyTask(raw);
    }
    
  • 非推奨:
    // ❌ 不正な項目を黙って無視します
    for (final raw in rawTasks) {
      if (raw case final Map<String, dynamic> taskMap) {
        _applyTask(taskMap);
      }
    }
    
4. 単一ケースの switch や真偽値の switch を避ける
  • ケースが 1 つだけで default: break; を含む switch 文ではなく、if (x is T) を使用します。
  • switch (condition) { true => a, false => b } ではなく、標準の条件付き三項演算子(condition ? a : b)を使用します。
5. 複数フィールドの展開にはオブジェクトの分解を使用し、単一プロパティやそのまま渡す場合には使用しない

複数の文で使用するフィールドを3つ以上取り出す場合、map.entries のループで MapEntry(:key, :value) を反復処理する場合、1回しか使わない中間変数をなくす場合、または nullable フィールドをローカル変数に昇格する場合(:field!、または :field と != null)は、オブジェクトまたはレコードの分解を使用してください。次の場合は、標準的なプロパティ/要素アクセス(config.field)を使用してください。

  • 単一のプロパティを読み取る場合(user.name、または ProfileNode(callFrame: ...) とネストする代わりに final CpuCallFrame(:url, :line) = node.callFrame; を使用する場合)。

  • 短い getter/メソッドで1~2個のフィールドだけを展開する場合、またはフィールドを展開して単一のコンストラクターや関数呼び出しにそのまま渡すだけの場合。

  • 明示的なローカル型注釈(specify_nonobvious_local_variable_types)を付けるために、フィールドを再利用する回数がわずか(1~2回)なのに新しいライブラリのインポートが必要になる場合。

  • 名前を変更した getter を持つ長いクラス名により、2フィールドの分解が3行以上にわたる場合。

  • 対象変数が外側のスコープの変数に代入される場合(例:finally の前に db = res.$1; とする場合 — final (db, ...) は外側の db をシャドーイングします)、または final と変更可能な var のローカル変数が混在する場合。

  • 長さが1の場合もあるリストで .first と .last を読み取る場合([first, ..., last] には length >= 2 が必要です。複数分岐の if-case に置き換えないでください)。

  • 推奨:

    // ✅ 複数のフィールドを展開し、1つの宣言で非 null 性を保証する
    final RunSpec(:target, :device!, :verbose) = spec;
    
  • 避ける:

    // ❌ プロパティを繰り返し複数行で取り出す
    final target = spec.target;
    final device = spec.device!;
    final verbose = spec.verbose;
    
6. 単独のスカラー比較に if-case を使用しない

単独の条件には if (code case >= 200 && < 300) ではなく、標準的なブール演算子(if (code >= 200 && code < 300))を使用してください。関係パターンは複数分岐の switch テーブルに使用します。

ワークフロー

タスクの進捗:パターンマッチングの実装

複雑なパターンマッチングロジックを実装する際は、このチェックリストをコピーして進捗を記録してください。

  • 評価対象のデータ構造を特定する(JSON、List/Segments、Record、Class、Enum)。
  • 適切な switch 構文を選択する(値には Expression、副作用には Statement)。
  • 必要なパターンを定義する(Object、Map、List、Record)。
  • Variable パターンを使用して必要なデータを取り出す(var x、:var y、...final rest)。
  • パターンでは表現できないロジックには Guard 節(when condition)を適用する。
  • 一致しないケースは Wildcard(_)または default 節で処理する(sealed class を使用していない場合)。
  • 網羅性とデッドコードについて静的アナライザーを実行する(dart analyze)。
  • 省略されたキー(containsKey のセマンティクス)と明示的な null 値について、実行時の Map/JSON パターンの動作を検証する。
フィードバックループ 1:網羅性チェック(静的検証)

sealed クラスまたは enum を切り替える場合は、コンパイル時にすべてのサブタイプが処理されることを確認してください。

  1. アナライザーを実行する: dart analyze を実行します。
  2. エラーを確認する: 「The type 'X' is not exhaustively matched by the switch cases」や、到達不能なパターンアームに関する警告を探します。
  3. 修正する: 未処理のサブタイプに対応する Object パターンを追加するか、デフォルトのフォールバックやエラーが許容される場合は明示的なワイルドカード(_)アームを追加します。
フィードバックループ 2:実行時の Map と JSON のパターン検証

dart analyze では動的な Map<String, dynamic> のキーを静的に検証できないため、実行時のパターンのセマンティクスを明示的に検証してください。

  1. 省略されたキーと明示的な null: マップパターン {'key': String? val} は map.containsKey('key') をチェックします。JSON ペイロードで 'key' が省略されている場合、String? が nullable であっても、実行時にパターンは一致しません。検証済みのマップから、オプションのキーを直接取り出してください(map['key'] as String?)。
  2. 即時失敗のフォールバック: 一致しないマップ構造や不正なマップ構造が、if-case チェックで黙って失敗するのではなく、明示的な _ => throw FormatException(...) アームに到達することを確認してください。

例

多相 JSON デシリアライズ(判別共用体)

Map パターンと switch 式を使用して、タグ付き JSON ペイロードを検証し、sealed クラス階層を構築します。実行可能な実装については、examples/json_patterns.dart を参照してください。タグ付きの ApiResponse を SuccessResponse と ErrorResponse に解析する方法を示しています。

ネストされた JSON の検証とオプションフィールド

ネストされた Map パターンと List パターンを使用して、必須のスキーマ構造を検証し、コレクションを一度に抽出します。processUserPayload の実行可能な実装については、examples/json_patterns.dart を参照してください。

Map パターンはキーの存在(containsKey)を確認します。オプションの JSON キーがペイロード内で省略される可能性がある場合('key': null として明示的に渡されるのではなく)、パターンを使って必須キーを分解し、一致したサブ Map からオプションフィールドを直接抽出します。

リストとパスセグメントの分解

リストの残余要素(...final rest)を使った if-case を使用すると、2 つの分岐を持つ _ => null の switch や、.first と .skip(1) による手動インデックス指定を使わずに、プレフィックスを検証して残りの要素を抽出できます。

String? resolveAllowedAssetSubpath(List<String> segments) {
  if (segments case ['assets', ...final rest]
      when rest.isNotEmpty && !rest.contains('..')) {
    return rest.join('/');
  }
  return null;
}
代数的データ型(Sealed クラス)

Object パターンと switch 式を使用して、ファミリー型を網羅的に処理します。

sealed class Shape {}

class Square implements Shape {
  final double length;
  Square(this.length);
}

class Circle implements Shape {
  final double radius;
  Circle(this.radius);
}

// Switch expression guarantees exhaustiveness due to `sealed` modifier.
double calculateArea(Shape shape) => switch (shape) {
  Square(length: var l) => l * l,
  Circle(:var radius)   => math.pi * radius * radius,
};
変数の入れ替えと分解

変数代入パターンを使用して、値を入れ替えたり、一時変数を使わずにレコードフィールドを抽出したりします。

var (a, b) = ('left', 'right');
(b, a) = (a, b); // Swap values

// Destructuring a function return
var (name, age) = getUserInfo();
ガード節と論理和

パターンが一致した後に任意の条件を評価するには、when を使用します。

switch (shape) {
  case Square(length: var s) || Circle(radius: var s) when s > 0:
    print('Valid positive shape with dimension $s');
  case Square() || Circle():
    print('Zero or negative dimension shape');
}

コメント