🐦

DartにおけるData Classについて

2022/12/18に公開

こんにちは。
マネーフォワードMEでモバイルエンジニアをやっているおはぎと申します。

この記事は Money Forward Engineering 1 Advent Calendar 2022 の18日目の記事です。
17日目はnyafunta9858さんでもう迷わないCoroutines(記事版〜余談編〜)でした。

TL;DR

  • そんなものはない(関羽の画像)
  • ただし、Freezedっていう便利な自動生成ライブラリがある
    • なるほど、これは自分でやるには大変だな、とコードを読んで理解する
  • Dart標準実装でData Classのように使える機能がリリースされる兆しがある

Freezedとは

FreezedとはKotlinでいうところのData Classに相当するDartコードを自動生成してくれる便利なライブラリです。私自身もFlutterでのモバイルアプリ開発においてUIの状態を記述する際など、大いにFreezedの恩恵に預かっています。
Freezedは以下の動機で開発されたようです。

Dartは素晴らしいがmodelの定義が面倒だったりする。次のことをしないといけないしなあ。

  1. コンストラクタとプロパティの定義
  2. toString,==オペレータ、hashCodeのオーバーライド
  3. ObjectをクローンするためのcopyWithの実装
  4. シリアライズ/デシリアライズの処理

私自身Flutterで開発を始める以前はAndroidの開発を主にしていたので、KotlinのようにDataClassを用いてUIの状態を記述するのにどうしても慣れてしまっていて、この動機には大いに同意しています。
Kotlinでは

data class Feed(
  id: Int,
  content: String,
)

のように書けば済むコードを実現するにはDartでは大量の実装が必要になります。
しかし、Freezedを使うことによってその大量の記述を自動生成することができます。
FreezedはData Classだけでなく、Seaded Classや、Jsonシリアライズ/デシリアライズに相当するコードの自動生成もサポートしています。
https://pub.dev/packages/freezed#union-types-and-sealed-classes
https://pub.dev/packages/freezed#fromjsontojson

自動生成されたコードを見てみる

動機の1と2がどのように実現されているか、見てみます。
動機の4に当たるコードは若干記述が異なるため、簡略化のため今回生成も行いません。
以下のようなコードを準備し、 Freezedの自動生成を実行してみます。

flutterプロジェクトのルートで

flutter pub run build_runner build

を実行すると、DataClassに相当する機能をもつコードがsample.freezed.dartとして生成されます。
生成前のコードのままではsample.freezed.dartファイル_$Feedmixinと_Feedクラスが存在せず、コンパイルが通らない状態です。

生成されたコードがこちらです。
sample.g.dart

長い!めちゃくちゃ長いのでリンクのみにとどめました。

生成前のコードでは未定義だったmixin_$Feedは以下のように定義されています。

mixin _$Feed {
  int get id => throw _privateConstructorUsedError;
  String get content => throw _privateConstructorUsedError;

  (ignore: true)
  $FeedCopyWith<Feed> get copyWith => throw _privateConstructorUsedError;
}

各々のメンバのgetterとcopywithが定義されているのがわかります。
これらの実装は生成前のコードでは存在しなかった、_Feedクラスによりなされています。

abstract class _Feed implements Feed {
  factory _Feed({required final int id, required final String content}) =
      _$_Feed;

  
  int get id;
  
  String get content;
  
  (ignore: true)
  _$$_FeedCopyWith<_$_Feed> get copyWith => throw _privateConstructorUsedError;
}

各メンバーのgetterの実装はされてますが、基本的な実装の大部分は_$_Feedによってなされているようです。

class _$_Feed implements _Feed {
  _$_Feed({required this.id, required this.content});

  
  final int id;
  
  final String content;

  
  String toString() {
    return 'Feed(id: $id, content: $content)';
  }

  
  bool operator ==(dynamic other) {
    return identical(this, other) ||
        (other.runtimeType == runtimeType &&
            other is _$_Feed &&
            (identical(other.id, id) || other.id == id) &&
            (identical(other.content, content) || other.content == content));
  }

  
  int get hashCode => Object.hash(runtimeType, id, content);

  (ignore: true)
  
  ('vm:prefer-inline')
  _$$_FeedCopyWith<_$_Feed> get copyWith =>
      __$$_FeedCopyWithImpl<_$_Feed>(this, _$identity);
}

このコードで、動機1の「コンストラクタとプロパティの定義」と動機2の「toString,==オペレータ、hashCodeのオーバーライド」が実現されていることがわかります。
そこまで複雑なコードではないにせよ、自分で毎回実装するのはなかなか辛いものがありそうです。

流石に言語仕様として欲しくない?

欲し〜〜〜〜〜い!!!

免責:本稿執筆時点での情報であり、変更が加わる可能性がある情報を扱っています。

Dartd言語の仕様としても長いこと期待されているようで、2017年10月ごろにDataClassの実装を求めるIssueが立てられて、はや幾年という状態でした。
それに対して、Macrosという仕様が提案されています。
サンプルコードを見るに、次のような記述で、Data Classに相当する挙動を実現できるようです。

() // ユーザー定義のマクロ
class Feed {
  final int id;
  final String content;
}

めっちゃ便利...!!!

Freezedと違いコンパイル時に実行されるため、更新のたび(結構高頻度)にいちいちTerminalでbuild_runnerを叩く必要がなくなるのが特に良い気がします。
ただ、DataClassというマクロをユーザー(我々実装者)が定義することになりそう(サンプルはこちら)なので、ややテクニカルな実装を我々がしなくてはならないところは保守運用の観点で懸念点にはなりそうです。
マクロの仕様プロポーザルは非常に読み応えがありましたので一読することを(あまり強くなく)お勧めします。

先日発表された The road to Dart3ではあくまで「Dart3以降」と具体的な時期は言及されてなかったものの、Macrosの実装には取り組んでいて、さらなる機能に期待してください、と力強いメッセージがありました。
来年もDartの言語としての進化が楽しみです!

Discussion