メインコンテンツにスキップ

レコード

レコードは、名前がなく、不変で、集約型の型です。他のコレクション型と同様に、複数のオブジェクトを1つのオブジェクトにまとめることができます。他のコレクション型とは異なり、レコードは固定サイズで、異種型であり、型付けされています。

レコードは実際の値であり、変数に格納したり、ネストしたり、関数に渡したり受け取ったり、リスト、マップ、セットなどのデータ構造に格納したりできます。

レコード構文

#

レコード式は、括弧で囲まれた、名前付きまたは位置指定のフィールドのコンマ区切りリストです。

dart
var record = ('first', a: 2, b: true, 'last');

レコード型注釈は、括弧で囲まれた型のコンマ区切りリストです。レコード型注釈を使用して、返り値の型とパラメータの型を定義できます。たとえば、次の (int, int) はレコード型注釈です。

dart
(int, int) swap((int, int) record) {
  var (a, b) = record;
  return (b, a);
}

レコード式と型注釈のフィールドは、関数のパラメータと引数の仕組みを反映しています。位置指定フィールドは括弧の直接内側に配置されます。

dart
// Record type annotation in a variable declaration:
(String, int) record;

// Initialize it with a record expression:
record = ('A string', 123);

レコード型注釈では、名前付きフィールドは、すべての位置指定フィールドの後に、型と名前のペアの波括弧で囲まれたセクション内に配置されます。レコード式では、名前はコロンの後に各フィールド値の前に置かれます。

dart
// Record type annotation in a variable declaration:
({int a, bool b}) record;

// Initialize it with a record expression:
record = (a: 123, b: true);

レコード型の名前付きフィールドの名前は、レコードの型定義、またはその形状の一部です。名前の異なる名前付きフィールドを持つ2つのレコードは、異なる型を持ちます。

dart
({int a, int b}) recordAB = (a: 1, b: 2);
({int x, int y}) recordXY = (x: 3, y: 4);

// Compile error! These records don't have the same type.
// recordAB = recordXY;

レコード型注釈では、位置指定フィールドに名前を付けることもできますが、これらの名前はドキュメントのためだけであり、レコードの型には影響しません。

dart
(int a, int b) recordAB = (1, 2);
(int x, int y) recordXY = (3, 4);

recordAB = recordXY; // OK.

これは、関数宣言または関数typedefの位置指定パラメータに名前を付けることができるが、それらの名前は関数のシグネチャに影響しないのと似ています。

詳細と例については、レコード型レコードの等価性を参照してください。

レコードフィールド

#

レコードフィールドは、組み込みのゲッターを介してアクセスできます。レコードは不変であるため、フィールドにはセッターがありません。

名前付きフィールドは同じ名前のゲッターを公開します。位置指定フィールドは、名前付きフィールドをスキップして、$<position> という名前のゲッターを公開します。

dart
var record = ('first', a: 2, b: true, 'last');

print(record.$1); // Prints 'first'
print(record.a); // Prints 2
print(record.b); // Prints true
print(record.$2); // Prints 'last'

レコードフィールドへのアクセスをさらに合理化するには、パターンのページを参照してください。

レコード型

#

個々のレコード型の型宣言はありません。レコードは、フィールドの型に基づいて構造的に型付けされます。レコードの形状(フィールドのセット、フィールドの型、および名前(存在する場合))は、レコードの型を一意に決定します。

レコードの各フィールドには独自の型があります。フィールドの型は、同じレコード内で異なる場合があります。型システムは、レコードからアクセスされる場所であれば、各フィールドの型を認識します。

dart
(num, Object) pair = (42, 'a');

var first = pair.$1; // Static type `num`, runtime type `int`.
var second = pair.$2; // Static type `Object`, runtime type `String`.

同じフィールドセットを持つレコードを作成する2つの無関係なライブラリを検討してください。型システムは、ライブラリが互いに結合されていなくても、それらのレコードが同じ型であると理解しています。

レコードの等価性

#

2つのレコードは、同じ形状(フィールドのセット)を持ち、対応するフィールドが同じ値を持つ場合に等しいと見なされます。名前付きフィールドの順序はレコードの形状の一部ではないため、名前付きフィールドの順序は等価性に影響しません。

dart
(int x, int y, int z) point = (1, 2, 3);
(int r, int g, int b) color = (1, 2, 3);

print(point == color); // Prints 'true'.
dart
({int x, int y, int z}) point = (x: 1, y: 2, z: 3);
({int r, int g, int b}) color = (r: 1, g: 2, b: 3);

print(point == color); // Prints 'false'. Lint: Equals on unrelated types.

レコードは、フィールドの構造に基づいて hashCode および == メソッドを自動的に定義します。

複数返り値

#

レコードを使用すると、関数がまとめてバンドルされた複数の値を返すことができます。返り値からレコード値を取得するには、パターンマッチングを使用して、ローカル変数に値を分割します。

dart
// Returns multiple values in a record:
(String name, int age) userInfo(Map<String, dynamic> json) {
  return (json['name'] as String, json['age'] as int);
}

final json = <String, dynamic>{'name': 'Dash', 'age': 10, 'color': 'blue'};

// Destructures using a record pattern with positional fields:
var (name, age) = userInfo(json);

/* Equivalent to:
  var info = userInfo(json);
  var name = info.$1;
  var age  = info.$2;
*/

また、名前付きフィールドを使用してコロン : 構文でレコードを分割することもできます。これについては、パターン型のページで詳しく読むことができます。

dart
({String name, int age}) userInfo(Map<String, dynamic> json)
// ···
// Destructures using a record pattern with named fields:
final (:name, :age) = userInfo(json);

レコードを使用せずに関数から複数の値を返すこともできますが、他の方法には欠点があります。たとえば、クラスを作成するのははるかに冗長であり、List または Map のような他のコレクション型を使用すると型安全性が失われます。

単純なデータ構造としてのレコード

#

レコードはデータのみを保持します。それがすべて必要な場合は、新しいクラスを宣言する必要なしに、すぐに利用でき、使いやすいです。同じ形状を持つ単純なデータタプルのリストについては、レコードのリストが最も直接的な表現です。

たとえば、この「ボタン定義」のリストを見てみましょう。

dart
final buttons = [
  (
    label: "Button I",
    icon: const Icon(Icons.upload_file),
    onPressed: () => print("Action -> Button I"),
  ),
  (
    label: "Button II",
    icon: const Icon(Icons.info),
    onPressed: () => print("Action -> Button II"),
  )
];

このコードは、追加の宣言なしに直接記述できます。

レコードとtypedef

#

typedef を使用してレコード型自体に名前を付け、完全なレコード型を記述する代わりにそれを使用することを選択できます。この方法では、現在のリストのエントリのいずれもnull値を持っていなくても、一部のフィールドがnull(?)になる可能性があることを示すことができます。

dart
typedef ButtonItem = ({String label, Icon icon, void Function()? onPressed});
final List<ButtonItem> buttons = [
  // ...
];

レコード型は構造型であるため、ButtonItemのような名前を付けることは、構造型を参照しやすくするためのエイリアスを導入するだけです。({String label, Icon icon, void Function()? onPressed})

すべてのコードがエイリアスでレコード型を参照するようにすると、すべての参照を更新することなく、後でレコードの実装を変更することが容易になります。

コードは、単純なクラスインスタンスと同じように、指定されたボタン定義で機能させることができます。

dart
List<Container> widget = [
  for (var button in buttons)
    Container(
      margin: const EdgeInsets.all(4.0),
      child: OutlinedButton.icon(
        onPressed: button.onPressed,
        icon: button.icon,
        label: Text(button.label),
      ),
    ),
];

後でレコード型をクラス型に変更してメソッドを追加することを決定することもできます。

dart
class ButtonItem {
  final String label;
  final Icon icon;
  final void Function()? onPressed;
  ButtonItem({required this.label, required this.icon, this.onPressed});
  bool get hasOnpressed => onPressed != null;
}

または、拡張型に。

dart
extension type ButtonItem._(({String label, Icon icon, void Function()? onPressed}) _) {
  String get label => _.label;
  Icon get icon => _.icon;
  void Function()? get onPressed => _.onPressed;
  ButtonItem({required String label, required Icon icon, void Function()? onPressed})
      : this._((label: label, icon: icon, onPressed: onPressed));
  bool get hasOnpressed => _.onPressed != null;
}

そして、その型のコンストラクタを使用してボタン定義のリストを作成します。

dart
final List<ButtonItem> buttons =  [
  ButtonItem(
    label: "Button I",
    icon: const Icon(Icons.upload_file),
    onPressed: () => print("Action -> Button I"),
  ),
  ButtonItem(
    label: "Button II",
    icon: const Icon(Icons.info),
    onPressed: () => print("Action -> Button II"),
  )
];

繰り返しますが、そのリストを使用するコードを変更する必要はありません。

いずれかの型を変更すると、それを使用するコードは、仮定をしないように非常に注意する必要があります。型エイリアスは、それがエイリアスされている値がレコードであることを、それを使用するコードが参照として使用する際に、保護や保証を提供しません。拡張型も同様に、ほとんど保護を提供しません。クラスのみが完全な抽象化とカプセル化を提供できます。