Dart Record(レコード・タプル)

一、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はなく、フィールド値を変更できません。

  1. 名前付きフィールド.フィールド名で直接取得
  2. 位置指定フィールド$数値で取得。数値は名前付きフィールドを除外し、位置フィールドの順番だけをカウントする
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点です。

  1. 2つのRecordのシェイプが完全に一致
  2. 対応する位置フィールド・名前付きフィールドの値が全て一致

名前付きフィールドの記述順序は等値判定に影響しません。ただしフィールド名が異なると型自体が異なるため必ず不一致となります。

// 位置フィールド、シェイプ同一かつ値一致 → 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を使用しない場合、下記の手間が発生します。

  1. クラスを新規作成:コンストラクタ・フィールド定義などコードが冗長になる
  2. Listで戻す:型安全性が失われ全てdynamic型となる
  3. 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. 方法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)
  1. 方法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種類の切り替えいずれも、画面でボタンをループ表示する業務コードは一切変更不要です。

十、まとめ

  1. 対応バージョン:Dart 3.0以上
  2. 3大核心特徴:匿名・不変・集約型;長さ固定・異種型格納・強い型制約
  3. フィールド2種類:位置フィールド$1/$2、名前付きフィールド.name
  4. 型判定ルール:構造シェイプで型決定、名前付きフィールド名がシェイプに含まれる;位置フィールドの別名はコメントのみ
  5. 等値比較:シェイプ完全一致かつ全フィールド値一致でtrue
  6. 主なメリット:関数複数戻り値、軽量データコンテナ、完全な型安全性
  7. 拡張手段:typedefで型別名を簡略化、後からClass/Extension Typeにシームレス切り替え可能

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です