2021/11/03

Dartのコンストラクタについて

こんにちは、ばんがです。
最近趣味でFlutterを触っており、必然的にDartにも触れることになるのですが、Dartのコンストラクタはいろんな書き方や種類があり少しわかりづらかったので備忘録もかねてまとめようと思います。

Generative Constructors

生成的コンストラクタ(と訳すようです)は、コンストラクタ名・引数・リダイレクト句・Initializer List・ボディから成るコンストラクタです。
基本的には一般的にイメージするコンストラクタと同じものですが、Dart特有の記法や機能があります。


まずは基本的なコンストラクタです。
一般的なコンストラクタの書き方と同じです。
thisは現在のインスタンスの参照で、通常名前が衝突する場合などに使用されます。

class Point {
  double x = 0;
  double y = 0;

  Point(double x, double y) {
    this.x = x;
    thix.y = y;
  }
}


また、代入をより簡易に記述するためにシンタックスシュガーが用意されています。
以下のコードは、コンストラクタのボディ内で代入する先程のコードと同等です。

class Point {
  double x = 0;
  double y = 0;

  Point(this.x, this.y);
}


デフォルトコンストラクタ

コンストラクタを宣言しない場合、Dartが自動的にデフォルトのコンストラクタを提供してくれます。
デフォルトのコンストラクタは引数を持たず、スーパークラスの引数のないコンストラクタを呼び出します。

名前付きコンストラクタ

以下のように名前付きのコンストラクタを宣言することができます。
コンストラクタに名前をつけることで、複数のコンストラクタを作成することができます。

class Point {
  double x, y;
  Point.origin1(this.x, this.y);
  Point.origin2() : x = 1, y = 1;
}

var p1 = Point.origin1(1, 1);
var p2 = Point.origin2();


スーパークラスの任意のコンストラクタを呼び出す方法

通常サブクラスのコンストラクタは、ボディの実行前にスーパークラスのデフォルトコンストラクタ(名前なし・引数なしのコンストラクタ)を呼び出す仕様です。

class Super {
  Super() {
    print('super default');
  }
}

class Sub extends Super {
  Sub.fromJson(json) {
    print('sub');
  }
}

void main() {
  var sub = Sub.fromJson({});
}

main();
// => super default
// => sub


さらに、Initializer Listがあれば呼び出される順序は以下のようになります。

  1. イニシャライザリスト
  2. スーパークラスの引数なしコンストラクタ
  3. サブクラスの引数なしコンストラクタ


スーパークラスのデフォルトコンストラクタ以外のコンストラクタを呼び出したい場合は、コロンをつけて、ボディの前に呼び出したいスーパークラスのコンストラクタを記述します。
(スーパークラスに名前なし、引数なしのコンストラクタが存在しない場合、この方法でいずれかのコンストラクタを呼び出さないとエラーになる)

class Super {
  double x = 0;
  Super.fromJson(double num) {
    x = num;
  }
}

class Sub {
  double y = 0;
  Sub(this.y) : super.fromJson(1)
}


コンストラクタへの引数は、コンストラクタが呼び出される前に評価されるので、以下のように関数の呼び出しもできる。
ただし、この引数はthisを参照することはできないので、staticメソッドは呼べるがインスタンスメソッドを呼ぶことはできないようです。

class Sub {
  double y = 0;
  Sub(this.y) : super.fromJson(someMethod())
}


Initializer Listとは?

Dart独自の機能で、コンストラクタのボディが実行される前に何らかの処理を実行することができます。
例えば、インスタンス変数を初期化したり、スーパークラスのコンストラクタを呼び出したりなどです。

Initializer Listを使うと、finalなインスタンス変数の初期化が簡単に行えます。

class Point {
  final double x, y;
  Point(double x, double y) : this.x = 0, this.y = 0 {
    ...
  }
}


以下のような書き方だとエラー。
ボディ実行時点でxはfinalで初期化されており、代入など変更ができないためだと思われます。

class Point {
  final double x;
  Point(double x) {
    this.x = x;
  }
}

var p = Point(0.1);
// Error: The setter 'x' isn't defined for the class 'Point'.


シンタックスシュガーを用いた代入であれば問題ない様子。

class Point {
  final double x;
  Point(this.x);
}

var p = Point(0.1);
print(p.x);
// 0.1


先程のスーパークラスの任意のコンストラクタの呼び出しも、Initializer Listの挙動と同じのようですね。

同じクラスの別のコンストラクタへリダイレクトする

あるコンストラクタから、同じクラスの別のコンストラクタへ処理を委譲することができます。
コロンに続けて、処理を委譲したいコンストラクタを呼び出します。
ただし、コンストラクタを直接呼び出すのではなく、以下のようにthisを使って呼び出す必要があります。

class Point {
  double x, y;
  Point(this.x, this.y);
  Point.another(double x, double y) : this.x = x * 10, this.y = y * 10;

  Point.redirect1(double x) : this(x, 0);
  Point.redirect2(double x) : this.another(x, 3);
}

var p1 = Point.redirect1(1);
var p2 = Point.redirect2(1);

print('${p1.x}, ${p1.y}');
// 1, 0

print('${p2.x}, ${p2.y}');
// 10, 30


また、Initializer Listと記法が似ていますが、thisを使ってリダイレクトしたときは、ボディを記述することはできないようです。

class Point {
  double x, y;
  Point(this.x, this.y);

  Point.redirect(double x) : this(x, 0) {
    ...
  }
}

// Error : Redirecting constructors can't have a body.


Constant Constructors

そのクラスが変化しないオブジェクトを作るときは、コンパイル時定数として生成することができます。
インスタンスをコンパイル時定数にするために利用するのがConstantコンストラクタです。

以下のようにコンストラクタにconstをつけて、全てのインスタンス変数をfinalで修飾します。

class ImmutablePoint {
  static const ImmutablePoint origin = ImmutablePoint(0, 0);
  final double x, y;
  const ImmutablePoint(this.x, this.y);
}


呼び出すときもconstが必要になります。

var ip = const ImmutablePoint(1, 1);


コンパイル時に定数オブジェクトをインスタンス化したい場合に利用するそうですが...。
いまいちまだ使い所がよくわからない🤔

Factory Constructors

必ずしもそのクラスの新しいインスタンスを返すわけではない場合に使うようです。
Factoryコンストラクタではインスタンスが自動的に生成されないので、例えばキャッシュデータを返したり、別のクラスのインスタンスを返したりすることができます。

以下のようにfactoryキーワードを事前につけることでファクトリコンストラクタを作ることができます。
(※Factoryコンストラクタはthisを参照できない)

class Logger {
  static final Map<String, Logger> _cache = <String, Logger>{};
  
  // キャッシュからインスタンスを返す
  factory Logger(String name) {
    return _cache.putIfAbsent(
      name, () => Logger._internal(name)
    );
  }
  Logger._internal(this.name);

  //別のコンストラクタを呼び出す
  factory Logger.fromJson(Map<String, Object> json) {
    return Logger(json['name'].toString());
  }
}


参考

https://dart.dev/guides/language/language-tour#constructors
https://dev.classmethod.jp/articles/about_dart_constructors/#toc-5
https://doitu.info/blog/5c10f5358dbc7a001af33ce5