一、Recordとは何か
Recordは匿名・不変・複合集約型で、複数の異なる型のデータを一つのオブジェクトにまとめるために使用します。List / Set / Mapとの主な違いは以下の通りです。
- 長さ固定:作成後、フィールドの数を増減できない、異種型格納:
- 同一Record内にint、String、boolなど完全に異なる型の値を格納可能
- 強い型制約:各フィールドの型がコンパイラに認識され、型安全性が失われない
- 読み取り専用・変更不可:作成後フィールドの値を再代入できない
Recordは完全に有効な変数で、変数への代入、関数引数・複数戻り値、ネスト、List/Map/Setへの格納など全ての一般的な操作に対応します。
二、Record基本文法
1. Recordリテラル
丸括弧で囲みカンマで区切り、位置指定フィールドと名前付きフィールドの2種類に分かれます。位置指定フィールドを先頭に記述し、名前付きフィールドは後ろにキー:形式で記載します。
// 位置指定フィールド + 名前付きフィールドの混在例
var info = ('Jim', age: 20, flag: true, 'Student');Code language: JavaScript (javascript)
2. Recordの使用方法
1 位置指定フィールド
// 宣言:2つの位置フィールド、順にString型、int型
(String, int) user;
// 構造に合わせて代入
user = ('李四', 22);
Code language: JavaScript (javascript)
2 名前付きフィールド
型宣言には{}を使用します
// a(int型)、b(bool型)の2つ名前付きフィールドを宣言
({int a, bool b}) data;
data = (a: 100, b: false);Code language: JavaScript (javascript)
3 混在使用
位置指定フィールドと名前付きフィールドを組み合わせて型を定義
// String型位置フィールド1つ、score(int)・pass(bool)の名前付きフィールド2つ
(String, {int score, bool pass}) exam;
exam = ('期末考', score: 90, pass: true);
Code language: JavaScript (javascript)
三、よくある誤解・注意点
1:名前付きフィールドの名称は型構造の一部、名称が異なれば完全に別の型となる
// 型:({int a, int b})
({int a, int b}) pointAB = (a: 1, b: 2);
// 型:({int x, int y})
({int x, int y}) pointXY = (x: 3, y: 4);
// コンパイルエラー!型が不一致のため相互代入不可
// pointAB = pointXY;
Code language: JavaScript (javascript)
名前付きフィールドの場合、両方int2つでもフィールド名がabとxyで異なるため代入できません。位置指定フィールドの場合は下記を参照。
2:位置指定フィールドに付けた別名はコメント扱い、型判定に影響しない
丸括弧内の位置フィールドに記載した名前は開発者向けドキュメントのみで、コンパイラは無視します。構造が一致すれば相互代入可能です。
(int a, int b) p1 = (10, 20);
(int x, int y) p2 = (30, 40);
// 正常実行、内部的に2つのint位置フィールドで型が完全一致
p1 = p2;Code language: JavaScript (javascript)
関数の仮引数と同じ考え方:引数名は関数シグネチャを変更しない、論理的に同一です。
四、Record内フィールドの読み取り
Recordは不変オブジェクトのためgetterのみ存在しsetterはなく、フィールド値を変更できません。
- 名前付きフィールド:
.フィールド名で直接取得 - 位置指定フィールド:
$数値で取得。数値は名前付きフィールドを除外し、位置フィールドの順番だけをカウントする
var record = ('first', a: 2, b: true, 'last');
print(record.$1); // 1番目の位置フィールド:first
print(record.a); // 名前付きフィールドa:2
print(record.b); // 名前付きフィールドb:true
print(record.$2); // 2番目の位置フィールド:last
Code language: PHP (php)
五、Record構造型判定ルール
Recordに個別のclass定義は存在せず、構造シェイプによって同一型か判定されます。
シェイプ = フィールド総数 + 各フィールドの型 + 名前付きフィールドの名称
シェイプが完全に一致すれば、ファイルやライブラリが異なっても同一の型とみなされます。
各フィールドの静的型はコンパイラが常に認識し、型情報が失われません。
// 1番目位置:num型、2番目位置:Object型
(num, Object) pair = (42, 'テキスト');
var firstVal = pair.$1; // 静的型num、実行時はint
var secondVal = pair.$2;// 静的型Object、実行時はStringCode language: JavaScript (javascript)
六、Recordの等値比較 ==
2つのRecordが等しいと判定される条件は2点です。
- 2つのRecordのシェイプが完全に一致
- 対応する位置フィールド・名前付きフィールドの値が全て一致
名前付きフィールドの記述順序は等値判定に影響しません。ただしフィールド名が異なると型自体が異なるため必ず不一致となります。
// 位置フィールド、シェイプ同一かつ値一致 → true
(int x, int y, int z) point = (1, 2, 3);
(int r, int g, int b) color = (1, 2, 3);
print(point == color); // true
// 名前付きフィールド名が異なり型不一致 → false
({int x, int y, int z}) p = (x:1, y:2, z:3);
({int r, int g, int b}) c = (r:1, g:2, b:3);
print(p == c); // falseCode language: PHP (php)
Recordは自動的に==演算子とhashCodeがオーバーライドされるため、手動実装する必要はありません。
七、関数の複数戻り値
Dartの関数は単一の戻り値しか返せないため、Recordで複数の異種型データをまとめ、パターン分解により一括で独立変数に展開できます。List/Mapの代替として最適(型安全性が保たれる)。
1 位置フィールドによる分解
// 戻り値:氏名(String)、年齢(int)
(String name, int age) getUserInfo(Map<String, dynamic> json) {
return (json['name'] as String, json['age'] as int);
}
void main() {
final userJson = {'name': 'Dash', 'age': 10, 'color': 'blue'};
// 一行で分解、2つの変数に展開
var (userName, userAge) = getUserInfo(userJson);
print(userName); // Dash
print(userAge); // 10
}
Code language: JavaScript (javascript)
2 名前付きフィールドによる分解
:フィールド名 記法を使用
({String name, int age}) getUserInfo(Map<String, dynamic> json) {
return (name: json['name'] as String, age: json['age'] as int);
}
void main() {
final userJson = {'name': 'Dash', 'age': 10};
// 名前指定分解記法
final (:name, :age) = getUserInfo(userJson);
print(name);
}
Code language: JavaScript (javascript)
List/Mapと比較したデメリット
C++、C#、Javaなど他言語でRecordを使用しない場合、下記の手間が発生します。
- クラスを新規作成:コンストラクタ・フィールド定義などコードが冗長になる
- Listで戻す:型安全性が失われ全てdynamic型となる
- Mapで戻す:キー名のタイプミスが発生しやすく、静的型検証が効かない
八、軽量構造化データ格納
メソッド不要でデータのみ保持する場合、新規クラスを定義せずRecordを直接利用可能。class定義の冗長コードを削減し、一覧系データに適しています。
サンプルコード
画面ボタン設定リスト(Flutterの例、Flutter学習後に理解できます)
import 'package:flutter/material.dart';
void main() {
final buttonList = [
(
label: "ファイルアップロード",
icon: const Icon(Icons.upload_file),
onPressed: () => print("アップロードボタンをクリック"),
),
(
label: "詳細を確認",
icon: const Icon(Icons.info),
onPressed: () => print("詳細ボタンをクリック"),
)
];
}
Code language: JavaScript (javascript)
九、typedefでRecord型に別名を定義
長い型定義を簡略化
Recordの型記述は長くなりがちなため、typedefで型別名を作成すると再利用が容易で、後からフィールド構造を一括変更しやすくなります。
基本的な使い方
// ボタン設定Recordに別名定義、onPressedはnull許容
typedef ButtonConfig = ({
String label,
Icon icon,
void Function()? onPressed
});
// 別名を直接使用しリスト宣言
List<ButtonConfig> buttons = [
(label: "送信", icon: const Icon(Icons.check), onPressed: () {}),
(label: "キャンセル", icon: const Icon(Icons.close), onPressed: null),
];
Code language: PHP (php)
拡張運用
後からデータにメソッドやロジックを追加する必要が生じた場合、画面のループ描画コードを修正せず型定義のみ差し替え可能です。
- 方法1:通常Classに置き換え
class ButtonConfig {
final String label;
final Icon icon;
final void Function()? onPressed;
ButtonConfig({required this.label, required this.icon, this.onPressed});
// 独自メソッド追加
bool get hasClickEvent => onPressed != null;
}
Code language: PHP (php)
- 方法2:Extension Typeで元のRecordをラップ
extension type ButtonConfig._(({String label, Icon icon, void Function()? onPressed}) _) {
String get label => _.label;
Icon get icon => _.icon;
void Function()? get onPressed => _.onPressed;
ButtonConfig({required String label, required Icon icon, void Function()? onPressed})
: this._((label: label, icon: icon, onPressed: onPressed));
bool get hasClickEvent => _.onPressed != null;
}
Code language: PHP (php)
上記2種類の切り替えいずれも、画面でボタンをループ表示する業務コードは一切変更不要です。
十、まとめ
- 対応バージョン:Dart 3.0以上
- 3大核心特徴:匿名・不変・集約型;長さ固定・異種型格納・強い型制約
- フィールド2種類:位置フィールド
$1/$2、名前付きフィールド.name - 型判定ルール:構造シェイプで型決定、名前付きフィールド名がシェイプに含まれる;位置フィールドの別名はコメントのみ
- 等値比較:シェイプ完全一致かつ全フィールド値一致でtrue
- 主なメリット:関数複数戻り値、軽量データコンテナ、完全な型安全性
- 拡張手段:typedefで型別名を簡略化、後からClass/Extension Typeにシームレス切り替え可能