こんにちは!FlutterやDartの開発現場で、日夜コードと格闘されている皆さん。先輩エンジニアの私です。
さて、Dart 3で導入された「パターンマッチング」と「レコード」のコンビ、本当に強力ですよね。`switch` 式や `case` 句を使って、美しく安全にデータを捌けるようになりました。でも、開発が進むにつれてこんな風に思ったことはありませんか?
「既存の型や、外部ライブラリのデータ構造に対して、もっと直感的にパターンマッチングさせたいな……」と。
実は、Dart 3.3で導入された Extension Types(拡張型) を使うと、これを実行時オーバーヘッドゼロ(ここ重要です!)で実現できてしまうんです。
今回は、この「Extension Typesによるパターンマッチングの拡張」という、一歩進んだテクニックを優しく、そして深く解説していきますね。ここをクリアすれば、あなたのDartコードは一段と洗練されますよ!
—
1. Extension Types と パターンマッチングの基本をおさらい
まずは、なぜこの2つを組み合わせると最強なのか、頭の中を整理しておきましょう。
そもそも Extension Types って何だっけ?
Extension Typesは、ざっくり言うと「ゼロコストのラッピング」です。
通常のクラス(`class`)でラップすると、ヒープ上に新しいオブジェクトが作られ、ガベージコレクション(GC)の負荷になりますよね。しかし、Extension Types(`extension type`)は、コンパイル時のみ存在する型です。実行時には、ラップしている元のデータ(Representation object)そのものとして扱われます。
Dartのパターンマッチング
Dartの `switch` 式や `if-case` では、データの構造をそのまま分解してマッチさせることができます。
// 例:リストの構造をパターンのようにマッチさせる
var pair = [10, 20];
var sum = switch (pair) {
[var a, var b] => a + b,
_ => 0,
};
この「構造を分解する力」と「Extension Typesのゼロコストな型付与」を組み合わせると、どうなるでしょうか?
—
2. 実践:独自の型安全なバリデーションルールを拡張する
例えば、APIから受け取った「生データのMap(`Map
ここに、Extension Typeを使ってパターンマッチングしやすい「インターフェース」をサクッと追加してみましょう。
// 1. 生のMapをラップする Extension Type を定義する
extension type ApiUserResponse(Map
// ゲッターを生やして、構造化の準備をする
String? get status => _data[‘status’] as String?;
int? get errorCode => _data[‘code’] as int?;
Map
}
この `ApiUserResponse` は、実行時にはただの `Map
パターンマッチングと組み合わせる
では、この拡張型に対して Dart 3 のパターンマッチングを使ってみましょう。
void handleApiResponse(Map
// 生データを Extension Type としてキャスト(これもコンパイル時解決なのでノーコスト!)
final response = ApiUserResponse(rawJson);
// switch式でスマートにパターンマッチング!
V8Engineの気分になって、データの状態を美しくさばきます
String message = switch (response) {
(status: ‘success’, payload: {‘username’: String name})
=> ‘ようこそ、$nameさん!’,
(status: ‘error’, errorCode: 401)
=> ‘認証エラーです。再ログインしてください。’,
(status: ‘error’, errorCode: var code)
=> ‘予期せぬエラーが発生しました (コード: $code)’,
_ => ‘未知のレスポンスです。’,
};
print(message);
}
あれ? ちょっと待ってください。さっきのコードの `switch` の中身、気づきましたか?
実はExtension Type単体では、直接プロパティ名を使ったオブジェクトパターン(`(status: ‘success’, …)` のような書き方)をそのままスッキリ書くには、少しボイラープレートが必要になります。
ここで、Dartの真骨頂である 「レコード(Records)」を返すプロパティ をExtension Typeに持たせるテクニックが活きてきます!
—
3. 応用:レコード返却メソッドでパターンを爆発的に美しくする
先ほどのコードを、さらに洗練させてみましょう。Extension Typeに「自分自身を分解しやすいレコードを返すメソッド」を生やしてあげるのです。
extension type ApiUserResponse(Map
// パターンマッチング用に、名前付きレコードを返すマッパーを用意する
(String? status, int? code, Map
return (
_data[‘status’] as String?,
_data[‘code’] as int?,
_data[‘payload’] as Map
);
}
}
これを使うと、`switch` 式でのパターンマッチングが圧倒的に美しく、かつ安全になります!
void handleApiResponseClean(Map
final response = ApiUserResponse(rawJson);
// レコードに対するパターンマッチングを実行
var result = switch (response.destructured) {
(‘success’, _, { ‘username’: String name })
=> ‘ログイン成功: $name’,
(‘error’, 401, _)
=> ‘セッションが切れました。’,
(‘error’, int code, _)
=> ‘エラーコード: $code が発生しました。’,
_ => ‘不正なデータ構造です。’,
};
print(result);
}
どうですか? この流れるような記述。
元のデータ構造(Map)のままだと安全性が低いし、かといって専用のクラスをわざわざ定義してファクトリーコンストラクタを書くのはボイラープレート(冗長なコード)が増えて面倒……。そんなジレンマを、「Extension Types × レコード × パターンマッチング」 の三位一体で鮮やかに解決できていますよね。
—
4. 陥りやすい罠と文法エラーの回避術
さて、ここで初心者の方がハマりがちなポイントをいくつか先回りして解説しておきますね。現場で「あれ、コンパイルエラーになる!?」となったら、ここを思い出してください。
罠1: Extension Typeのコンストラクタは「プライベート」にすべき場面が多い
Extension Typeは、ラップ元の型と異なる独自のバリデーションや意味を持たせたいことが多いです。適当に値を渡されては困る場合は、アンダースコア `_` を使ってコンストラクタを隠蔽(プライベート化)しましょう。
extension type const StrictEmail(String _email) {
// ファクトリーコンストラクタでバリデーションを強制する
factory StrictEmail.parse(String input) {
if (!input.contains(‘@’)) {
throw FormatException(‘無効なメールアドレスです’);
}
return StrictEmail(input);
}
}
こうすることで、信頼できるデータだけをパターンマッチングに回すことができます。
罠2: 実行時の型(Runtime Type)の錯覚に注意
「Extension Typeを使っているから、実行時には独自のオブジェクトになっているはずだ」と思って `response is ApiUserResponse` のような型チェックを多用すると、思わぬ挙動をすることがあります。
繰り返しになりますが、Extension Typeの実体はあくまでラップ元の型(この場合は `Map
—
まとめ
今回は、Dartの Extension Types と パターンマッチング を組み合わせて、既存の型を拡張し、直感的で安全なAPIを設計する高度なテクニックをご紹介しました。
- Extension Types は実行時コストゼロで既存の型に新しい振る舞いやビューを追加できる。
- レコードを返すプロパティやメソッドを生やすことで、複雑なデータの パターンマッチング が劇的に書きやすくなる。
- ボイラープレートを減らしつつ、型安全なドメインロジックを構築できる。
ここをクリアできれば、あなたのDartの引き出しはプロフェッショナルレベルの厚みになりますよ!ぜひ、今日の開発から試してみてくださいね。
それでは、また次回の知見でお会いしましょう。ハッピー・コーディング!