dart-use-pattern-matching について
dart-use-pattern-matchingは、Dart開発者向けにデータ、パス、sealed型でのパターンマッチングとswitchの使い方を示すコーディングスキルです。選択ルールに加え、避けるべき書き方、検証方法、例を含みます。
主な機能
- 01データ形状に応じたパターン選択
- 02switch式とswitch文の使い分け
- 03Map、List、Record、Objectパターン
- 04網羅性と実行時検証の確認
- 05実用的な利用のためのアンチパターン
活用シーン
- 01JSONデータのデシリアライズと検証
- 02URLやパスのセグメント解析
- 03sealed型のレスポンスを網羅的に処理
- 04複数の戻り値の分解
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ dart-lang/ skills --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 を切り替える場合は、コンパイル時にすべてのサブタイプが処理されることを確認してください。
- アナライザーを実行する:
dart analyzeを実行します。 - エラーを確認する: 「The type 'X' is not exhaustively matched by the switch cases」や、到達不能なパターンアームに関する警告を探します。
- 修正する: 未処理のサブタイプに対応する Object パターンを追加するか、デフォルトのフォールバックやエラーが許容される場合は明示的なワイルドカード(
_)アームを追加します。
フィードバックループ 2:実行時の Map と JSON のパターン検証
dart analyze では動的な Map<String, dynamic> のキーを静的に検証できないため、実行時のパターンのセマンティクスを明示的に検証してください。
- 省略されたキーと明示的な
null: マップパターン{'key': String? val}はmap.containsKey('key')をチェックします。JSON ペイロードで'key'が省略されている場合、String?が nullable であっても、実行時にパターンは一致しません。検証済みのマップから、オプションのキーを直接取り出してください(map['key'] as String?)。 - 即時失敗のフォールバック: 一致しないマップ構造や不正なマップ構造が、
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');
}