Dart 3.0で導入されたRecords型の解説

# はじめに

Dart 3.0で **Records**型が導入されました。 Dart 3.0は、Flutter 3.10で採用されているので、Flutter 3.10から **Records**型が使えます。

本記事では、この**Records**型について解説します。


## 対象読者

* FlutterやDartの経験がある人で、Dart 3.0の新機能に興味ある人
    * FlutterやDartの基本については説明しません。


## Records型を使ってみる

Records型を利用することにより、複雑な型を持つデータを簡単に作成できるようになります。
まずは、簡単な、名前(`String`)と年齢(`int`)を返すような関数(`userInfo`)を作る場合を例にあげて、Records型がどのように役に立つのかを見ていきます。


### `List`を使う

`userInfo`の返り値として `List`を使って、名前(`String`)と年齢(`int`)を返す場合のコードです。

```dart
// 名前と年齢を返す関数。
List<Object> userInfo() {
  return ['John', 23];
}

void main() {
  // Listで受け取る
  final List<Object> info = userInfo();
  final String name = info[0] as String; // type safeではない
  final int age = info[1] as int;

  print('name: $name, age: $age');
}
```

この場合、`userInfo`のAPIとしての次のようなお約束があります。

* Listのサイズが 2
* 0番目の要素の型は `String`
* 1番目の要素の型は `int`。

が、たとえ誤ったとしても、コンパイラエラーとならないので、問題の検出は実行時となってしまい、発見が遅れます。


### クラスを使う

`userInfo`の返り値として クラス(`UserInfo`)を使って、名前(`String`)と年齢(`int`)を返す場合のコードです。

```dart
// クラスを作る
class UserInfo {
  final String name;
  final int age;
  UserInfo(this.name, this.age);
}

UserInfo userInfo() {
  return UserInfo('John', 23);
}

void main() {
  final UserInfo info = userInfo();
  final String name = info.name;
  final int age = info.age;

  print('name: $name, age: $age');  
}
```
この場合、さきほどの Listを返すときの問題点はすべて解消されます。
しかしながら、クラス(`UserInfo`)を作成するという手間がかかります。


### Records型を使う

`userInfo`の返り値として Records型を使う場合のコードです。


```dart
(String, int) userInfo() {
  return ('John', 23);
}

void main() {
  final (String, int) info = userInfo();
  final String name = info.$1;
  final int age = info.$2;

  print('name: $name, age: $age');
}
```

*クラスを使う* と似ていますが、クラス作成をしていないだけ、短くなっています。


## Records型とは

### Record 文法

Records型は、無名な、イミュータブルな、アグリゲートな型です。

* 無名: クラスのように別途作成する必要がありません。その場で作成できます。
* イミュータブル: 作成したRecords型の値を変更することはできません。
* アグリゲート: 複数の型をまとめることができます。


Records型は、次のように表現します。
* フィールドは、コンマで区切る。
* 全フィールドを()で囲む。
* 2種類のフィールド
    * ポジションフィールド 
    * 名前付きフィールド

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

この例では、次のように２種類のフィールドが混在しています。

* `'first'` : １番目のポジションフィールド。 `String`
* `a: 2`: 名前`a`を持つ、名前付きフィールド。`int`
* `b: true` :  名前`b`を持つ、名前付きフィールド。`bool`
* `laｓt`: ２番めのポジションフィールド。 `String`


ポジションフィールドのRecords型
```dart
// Records型 recordの宣言。 
(String, int) record;

// recordの初期化
record = ('A string', 123);

// 宣言と初期化をまとめてもよい
// (String, int) records = ('A string', 123);

// $1で １番目の要素にアクセス
print(record.$1);  // 'A string`
// $2で 2番目の要素にアクセス
print(record.$2);  // 123
```

名前付きフィールドのRecords型
```dart
// Records型 recordの宣言。 
({int a, bool b}) record;

// recordの初期化
record = (a: 123, b: true);

// 宣言と初期化をまとめてもよい
// ({int a, bool b}) records = (a: 123, b: true);

print(record.a);  // 123
print(record.b);  // true
```


名前付きフィールドのRecords型では、フィールド名が違う場合は、別の型となります。
```dart
({int a, int b}) recordAB = (a: 1, b: 2);
({int x, int y}) recordXY = (x: 3, y: 4);

// recordABとrecordXYは別の型とみなされるので、代入するとエラーなる。
// recordAB = recordXY;
```

ポジションフィールドの場合は、同じ型となります。
```dart
// ポジションフィールド場合、a, bは、ドキュメンテーションなので、型に影響しない
(int a, int b) recordAB = (1, 2);
(int x, int y) recordXY = (3, 4);

// 同じ型なので代入できる。
recordAB = recordXY; // OK.
```


### Record フィールド

名前付きフィールドは、名前でアクセスします。
一方、ポジションフィールドは、`$<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'
```


### Record と Pattern

パターンで各要素を取り出すことができる。

ポジションフィールドの場合
```dart
// ポジションフィールドのRecordsを返す関数
(String, int) userInfo() {
  return ('John', 23);
}

void main() {
  // 一旦変数(info)で受ける
  final info = userInfo();
  // で、各要素を取り出す。
  print(info.$1);
  print(info.$2);

  // パターンを使って、各要素を直接取り出す
  final (name, age) = userInfo();
  print(name);
  print(age);  
}

```

名前付きフィールドの場合
```dart
// 名前付きフィールドのRecordsを返す関数
({String name, int age}) userInfo() {
  return (name: 'John', age: 23);
}

void main() {
  // 一旦変数(info)で受ける
  final info = userInfo();
  // で、各要素を取り出す。
  print(info.name); 
  print(info.age);

  // パターンを使って、各要素を直接取り出す
  final (:name, :age) = userInfo();
  print(name);
  print(age);

  // (:name, :age)は、 (name: name, age: age)の省略形
  // 別変数で取り出したい場合は、次のように書く
  //final (name :name1, age :age1) = userInfo();
  //print(name1);
  //print(age1);
  
}

```


## あとがき

Records型の概要をざっくりと説明しました。
興味を持った人は、[Codelabs](https://codelabs.developers.google.com/codelabs/dart-patterns-records?hl=ja#0)をやってみましょう。

Dart 3.0で同じ導入されたPatternについては、最後に軽くふれた程度ですが、Recordsと合わせて使うと非常に強力な武器になるので、Patternについては、別途書こうと思います。

[こちらに書きました。](https://qiita.com/kenma/items/1fc3891b3a197f535522)

## 参考

* https://dart.dev/language/records
* https://codelabs.developers.google.com/codelabs/dart-patterns-records