dart-3-updates について
Dart 3 の言語機能を使う Dart 開発者向けのスキルガイドです。分岐、パターン、レコードの活用方法と、既存コードを更新する手順を扱います。
主な機能
- 01switch と if-case の分岐
- 02パターンマッチと分解
- 03レコードと適用場面の理解
- 04sealed 型による網羅性チェック
- 05既存の Dart コードの段階的な更新
活用シーン
- 01if-else 連鎖や switch 文のリファクタリング
- 02コレクションやオブジェクトからの分解
- 03レコードとデータクラスの選択
- 04コードを Dart 3 の書き方に更新
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ evanca/ flutter-ai-rules --skill dart-3-updatesインストール後、Claude Code で/dart-3-updatesを入力して読み込まれたか確認します。
詳細
Dart 3 の更新スキル
Dart 3 の言語機能(分岐、パターン、パターンの種類、レコード)を正しく、慣用的に適用します。
使用する場面
次のような場合に、このスキルを使用します。
switch文やif-elseチェーンを記述またはリファクタリングするとき。- データを保持する新しいクラスを作成し、シールドクラス、レコード、通常のクラスから選択するとき。
- マップ、リスト、レコード、オブジェクトから値を分解するとき。
- パターン、網羅性チェック、または switch 式を使って、Dart 3 より前のコードを最新化するとき。
1. 分岐
if / if-case
// Standard if
if (score >= 90) {
grade = 'A';
} else if (score >= 80) {
grade = 'B';
} else {
grade = 'C';
}
// if-case: match and destructure against a single pattern
if (pair case [int x, int y]) {
print('$x, $y');
}
ifの条件はboolに評価される必要があります。if-caseでは、パターン内で宣言された変数は、一致した分岐内でのみ有効です。- パターンが一致しない場合、(存在すれば)制御は
else分岐に移ります。
switch 文
switch (command) {
case 'quit':
quit();
case 'start' || 'begin': // logical-or pattern
startGame();
default:
print('Unknown command');
}
- 一致した
caseの本体が実行され、末尾に移ります。breakは不要です。 - 空でない case は、
continue、throw、またはreturnで終了できます。 - 一致しない値を処理するには、
defaultまたは_を使用します。 - 空の case はフォールスルーします。空の case でのフォールスルーを防ぐには、
breakを使用します。 - 順番どおりでないフォールスルーには、ラベル付きの
continueを使用します。 - 複数の case で本体を共有するには、論理 OR パターン(
case a || b)を使用します。
switch 式
final color = switch (shape) {
Circle() => 'red',
Square() => 'blue',
_ => 'unknown',
};
caseは省略し、本体には=>を使用し、各 case はカンマで区切ります。- デフォルトには
defaultではなく_を使用します。 - 値を生成します。
網羅性
- Dart はコンパイル時に、
switch文と式の網羅性をチェックします。 - 網羅性を満たすには、
default/_、列挙型、またはsealed型を使用します。
sealed class Shape {}
class Circle extends Shape {}
class Square extends Shape {}
// Dart knows all subtypes — no default needed:
String describe(Shape s) => switch (s) {
Circle() => 'circle',
Square() => 'square',
};
ガード節
switch (point) {
case (int x, int y) when x == y:
print('Diagonal: $x');
case (int x, int y):
print('$x, $y');
}
- パターンの後に
when conditionを追加すると、一致条件をさらに絞り込めます。 if-case、switch文、switch式で使用できます。- ガードが
falseの場合、次の case に処理が進みます。
2. パターン
パターンは、一致判定と分解のために値の形状を表します。
用途
// Variable declaration
var (a, [b, c]) = ('str', [1, 2]);
// Variable assignment (swap)
(b, a) = (a, b);
// for-in loop destructuring
for (final MapEntry(:key, :value) in map.entries) { ... }
// switch / if-case (see Branches section)
- ワイルドカード
_は、一致した値の一部を無視します。 - リストパターンの残余要素(
...)は、残りの要素を無視します。 - case パターンは失敗可能です。一致しない場合、次の case に処理が進みます。
- case 内で分解された値は、その case の本体内でのみ有効なローカル変数になります。
オブジェクトパターン
var Foo(:one, :two) = myFoo;
JSON/ネストされたデータの検証
if (data case {'user': [String name, int age]}) {
print('$name, $age');
}
3. パターンの種類
| パターン | 構文 | 説明 |
|---|---|---|
| 論理 OR | p1 || p2 | いずれかの分岐が一致すると一致します。すべての分岐で同じ変数をバインドする必要があります。 |
| 論理 AND | p1 && p2 | 両方が一致すると一致します。変数名が重複してはいけません。 |
| 関係 | == c、< c、>= c | 値を定数と比較します。範囲を表すには && と組み合わせます。 |
| キャスト | subpattern as Type | 型を検証してから、内側のパターンを照合します。型が一致しない場合は例外がスローされます。 |
| Null チェック | subpattern? | null 以外に一致し、null 非許容型をバインドします。 |
| Null アサーション | subpattern! | null 以外に一致し、一致しない場合は例外がスローされます。宣言で使用すると、null を排除できます。 |
| 定数 | 42、'str'、const Foo() | 値が定数と等しい場合に一致します。 |
| 変数 | var name、final Type name | 一致した値を新しい変数にバインドします。型付き形式は、宣言された型に一致する場合のみ一致します。 |
| ワイルドカード | _、Type _ | 値をバインドせずに、任意の値に一致します。 |
| 括弧付き | (subpattern) | 優先順位を制御します。 |
| リスト | [p1, p2] | 位置でリストに一致します。残余要素を使用しない場合、長さも一致する必要があります。 |
| 残余要素 | ...、...rest | 任意の長さの末尾部分に一致するか、残りの要素を収集します。 |
| マップ | {'key': subpattern} | キーでマップに一致します。キーが存在しない場合、StateError がスローされます。 |
| レコード | (p1, p2)、(x: p1, y: p2) | 形状でレコードに一致します。推論できる場合、フィールド名は省略できます。 |
| オブジェクト | ClassName(field: p) | 型で一致し、ゲッターを通じて分解します。追加のフィールドは無視されます。 |
- 優先順位の低いパターンをグループ化するには、括弧を使用します。
- すべてのパターンの種類は、ネストして組み合わせることができます。
4. レコード
// Create
var record = ('first', a: 2, b: true, 'last');
// Type annotation
({int a, bool b}) namedRecord;
// Access
print(record.$1); // positional: 'first'
print(record.a); // named: 2
- レコードは匿名、不変、固定サイズの集約です。
- 各フィールドは異なる型を持つことができます(異種)。
- フィールドには組み込みゲッター(
$1、$2、.name)を介してアクセスします。セッターはありません。 - 同じ形状で、フィールド値が等しい場合、2 つのレコードは等しいとみなされます。
hashCodeと==は自動的に定義されます。
複数の戻り値
(String name, int age) userInfo(Map<String, dynamic> json) {
return (json['name'] as String, json['age'] as int);
}
var (name, age) = userInfo(json);
// Named fields:
final (:name, :age) = userInfo(json);
レコードとデータクラスの比較
次の場合はレコードを使用します。
- 1 つの関数から複数の値を返す場合(小規模で、一度限りの使用)。
- コードベース内で再利用しない少数の値を、ローカルにまとめる場合。
- 追加の振る舞いを持たずに、構造的な等価性が必要な場合。
次の場合はクラスを使用します。
- 複数のファイルや機能で型を再利用する場合。
- メソッド、カプセル化、継承、または
copyWithが必要な場合。 - 型が公開 API または長期にわたって使用されるデータモデルの一部である場合。
- 形状の変更を、コードベース全体で型システムに検出させる必要がある場合。
その他のベストプラクティス
- 可読性と保守性を高めるために、レコード型には
typedefを使用します。 - レコード型のエイリアスを変更しても、コードベース全体で型安全性が保証されるわけではありません。完全な抽象化を提供するのはクラスだけです。
5. 移行ワークフロー
Dart 3 より前のコードを最新化する場合は、次の手順に従います。
ステップ 1 — if-else チェーンを switch 式に置き換える
// Before (pre-Dart 3)
String label;
if (status == Status.loading) {
label = 'Loading...';
} else if (status == Status.success) {
label = 'Done';
} else {
label = 'Error';
}
// After (Dart 3)
final label = switch (status) {
Status.loading => 'Loading...',
Status.success => 'Done',
Status.error => 'Error',
};
ステップ 2 — 抽象クラスの階層を sealed クラスに変換する
// Before
abstract class Result {}
class Success extends Result { final String data; Success(this.data); }
class Failure extends Result { final String error; Failure(this.error); }
// After — enables exhaustive switch
sealed class Result {}
final class Success extends Result { const Success(this.data); final String data; }
final class Failure extends Result { const Failure(this.error); final String error; }
ステップ 3 — 複数の戻り値に分割代入を使用する
複数の値を返すことだけを目的とするラッパークラスを、レコードに置き換えます。
ステップ 4 — 検証する
変更を加えるたびに dart analyze を実行し、網羅性と型安全性を確認します。
コメント
Markdown 対応