Dart 3がもたらした変革:レコードとパターンマッチングで「型」を再定義する
Dart 3のリリースは、単なるマイナーアップデートではない。これはDart VMの設計思想、およびコンパイラが「データ」をどう扱うかという基本原則におけるパラダイムシフトだ。
かつて我々は、2つ以上の値を返したいだけで、わざわざ名前付きのクラスを定義するか、型安全性を捨てて`Map
テクニカルリードとして、君たちが今日から書くべき「真に堅牢で効率的なコード」の設計指針を授ける。
—
1. なぜ「クラス」ではなく「レコード」なのか
従来のDartにおいて、クラスは「アイデンティティ(参照)」を持つ重いオブジェクトだった。一方、レコードは構造的な型(Structural Typing)であり、値そのものが型を決定する。
内部的な最適化の視点
Dart VMにおいて、レコードは極めて軽量に扱われる。クラスのようなVtable(仮想関数テーブル)のルックアップを必要とせず、コンパイラはレコードのフィールドが不変であることを前提に、レジスタへの割り当てやインライン化をより積極的に行うことができる。
// 従来の冗長なアプローチ:たかだか座標のためにクラスを定義
class Point {
final double x;
final double y;
Point(this.x, this.y);
}
// Dart 3:レコードによる構造化
// メモリ上のオーバーヘッドが少なく、等価性比較も自動で行われる
(double x, double y) getPoint() => (10.0, 20.0);
教訓:
「振る舞い(メソッド)」を持たない純粋なデータの受け渡しにクラスを使うのは、現代のDartにおいては「過剰設計」だ。
—
2. 変数宣言の極致:分割代入とパターンマッチング
レコードの真価は、変数宣言時のDestructuring(分割代入)で発揮される。APIレスポンスのパースや、コンポーネントのProps管理において、この記法はバグの混入を劇的に減らす。
プロダクション級のコード例:API連携のステータス管理
以下のコードは、Webフロントエンド開発で頻出する「非同期通信の結果とメタデータを同時に受け取る」シナリオだ。
/// APIレスポンスとメタ情報を同時に返す関数
/// 以前なら専用のResponseクラスを作っていたが、レコードなら1行で済む
Future<(List
// … 非同期処理 …
final List
final int total = 100;
return (users, total); // カッコで囲むだけでレコードとしてリターン
}
void updateUI() async {
// 【極限の知見】宣言と同時に分割代入。
// final (users, total) ではなく、型を明示することでコンパイラの型推論を助け、可読性を担保する
final (List
print(‘Fetched ${users.length} of $totalCount’);
}
—
3. シールドクラスとパターンマッチングの「黄金コンビ」
実務において最も強力なのは、`sealed class`とレコード、そして`switch`式を組み合わせた設計だ。これはコンパイル時に網羅性チェック(Exhaustiveness check)が働くため、実行時の`Unexpected Error`を物理的に排除できる。
実践:堅牢なコンポーネント設計
// 状態を定義:sealedにより、これ以外の状態が存在しないことをコンパイラが保証する
sealed class AuthState {}
class Authenticated extends AuthState {
final (String name, String email) user; // レコードをフィールドに持つ
Authenticated(this.user);
}
class Unauthenticated extends AuthState {}
class Loading extends AuthState {}
// — UIロジックでの利用 —
String getWelcomeMessage(AuthState state) {
// switch文が「式」として評価される。
// 全ての状態を網羅していない場合、コンパイルエラーになる。これが最強のガードだ。
return switch (state) {
Authenticated(user: (var name, _)) => ‘ようこそ、$nameさん’, // レコードの特定フィールドのみ抽出
Unauthenticated() => ‘ログインしてください’,
Loading() => ‘読み込み中…’,
};
}
—
4. パフォーマンス上の注意点:レコードを乱用してはいけないケース
レコードは万能ではない。チーフアーキテクトとして、以下のケースではレコードではなくクラスの採用を推奨する。
1. ドメインの境界を越える場合:
レコードは「名前」を持たない。`(String, String)` というレコードが「姓名」なのか「メールとパスワード」なのかは、文脈に依存する。大規模なプロジェクトで複数のレイヤー(Data層からUI層など)を跨いでデータを受け渡す場合は、型定義(class)を行い、セマンティクスを明確にすべきだ。
2. 不変条件(Invariants)のバリデーションが必要な場合:
レコードはコンストラクタでロジックを実行できない。値の範囲チェックや整合性バリデーションが必要なら、`factory`コンストラクタを持つクラスに軍配が上がる。
—
結論:変数の「宣言」は「意図」の表明である
Dart 3以降、`var`, `final`, `const` に加えて、レコードによる「構造の宣言」が武器に加わった。
- 一時的なデータの集約にはレコードを使え。
- 戻り値が複数ある場合に`Map`を使うのは今日限りでやめろ。
- 制御フローにはパターンマッチングを使い、コンパイラに網羅性を検証させろ。
これらは単なる書き方の好みの問題ではない。実行時のオーバーヘッドを最小化し、コンパイル時の安全性を最大化するための、プロフェッショナルなエンジニアとしての「規律」である。
君たちのコードが、よりシャープで、より堅牢なものになることを期待している。