Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
Help us understand the problem. What is going on with this article?

Dartの定数は定数を内包できない? ⇒ Dart 2で出来るようになりました ⇒ なってませんでした

More than 1 year has passed since last update.

アップデート (2018.9.24)

初期化リスト内でのconstコンストラクタ呼び出しは未サポートだそうです。
理由は、(たぶんDartVMでの)起動を遅くしないため。
四則演算等の極限られた定数式のみが許されるようです。

ちなみに私のissueは、将来エンハンス要望が高まり、状況が許す場合に備えて、openしておくそうです。

アップデート (2018.9.22)

Dart 2で改善されました。
基本的には代替案2が採用され、newも省略で万事(?)解決です。

class C {
  final x;
  const C(this.x);
}

class D {
  final C y;
  const D(x):y = C(x); // コンパイル時エラー! ではなくなった。
}

main(){
  const d = D(1);
  print(d.y.x);
}

DartPadで実行

4年の歳月を経てようやくこれが実現されたのは、Dartがバージョン2でコンパイルを重視した型安全な処理系に変わり、静的に深い解析を行うようになったのが要因だと思います。

とはいえ、まだDartPadでは変なエラーメッセージを出したり、コメントの有る行をconst D(x):y = const C(x);と書いても通るなど、まだ動きが怪しいですが。

まえおき

もちろん出来ますが、一見普通なのに通らないケースもあります。
まずは予備知識から。

static, final, constの違い

Dartではstaticfinalconstはそれぞれ異なる意味を持っています。 Dart News & Updates: Const, Static, Final, Oh my!

キーワード 修飾対象 説明
static メンバシップ クラス定義でstaticと書くとクラスメンバになります。newしなくとも(変数:リファレンスが)存在するという意味で静的です。ただし、常に静的な存在のトップレベル変数では明示的にstaticを指定するとエラーになります。初期化の実行は なんと 最初の参照時です。
final 変数 1度だけ「代入」可能です。「代入」は初期化で行う必要があります。その後は変更不可です。厳密には初期化要、代入不可。初期化は通常のインスタンスメンバならnewの時、クラスメンバ(static final)やトップレベル変数なら先述の通り最初に参照した時、自分を包含するクラスのインスタンスが定数なら後述の通りコンパイル時です。
const コンパイル時定数値です。コレクションやユーザ定義クラスの定数オブジェクトを定義できますが、その場合、その値は「deep」にコンパイル時に決定的であることが必要です。値はcanonicalizeされます。(場合によっては、最適化がcanonicalizeされた唯一のオブジェクトも消しそう。)

定数コンストラクタ

Dartにはユーザ定義クラスの定数インスタンスを作るために定数コンストラクタがあります。

class C {
  final x;
  const C(this.x);
}

対象となるクラスのインスタンスメンバ変数は全てfinalである必要があります。
そして、定数を定義する場合にはnewの代わりにconstを用います。

var c = const C(1); // リファレンスは変数
const C = const C(2); // リファレンスも定数 (最適化で消されるかも)

コレクションを持つ定数

コレクションを持つユーザ定義クラスも定数化できます。

class CC {
  final x;
  final List l;
  const CC(this.x, this.l);
}

var cc = const CC(1, const[true, false, const['a', 'b', 'c']]); // const[...]に注目

先述の通り、メンバのメンバも遷移的、再帰的にコンパイル時に決定的な定数式です。

本題

ユーザ定義クラスを持つ定数は作れない?

次に、ユーザ定義クラスを持つユーザ定義クラスの定数を宣言してみます。

class D {
  final C y;
  const D(x):y = const C(x); // コンパイル時エラー!
}

ダメです。

なぜダメか

Dart Editorのメッセージは、下記の通り。

Arguments of a constant creation must be constant expressions

ここで言うArgumetとはD(x)xではなくて、C(x)xです。

ECMA-408 10.6.3 Constant Constructors
によると、xは「潜在的定数式」であって「コンパイル時定数」ではないのでconst C()の引数には使えないよ、とのことです。

納得です...ん?

なぜ、なぜダメか

xは定数コンストラクタD() の仮引数なんだから、実引数も定数って仮定してOKではないのか?

ところが、実は、定数コンストラクタはconstのみならず、newで呼び出せるのです。

var cv = new C(1); // コンパイル時じゃないよ

そうだとすると、

var v;
// :
// :
var dv = new D(v);

のようなことが出来てしまい、この場合、

  const D(x):y = const C(x) // xは変数vじゃねーか!!

のようなことが静的に想定されるので、コンストラクタ定義がコンパイル時エラーということです。

なぜコレクションは大丈夫だったか?

コンストラクタ定義ではなく、コンストラクタ呼び出しにおいて、メンバであるコレクションをリテラルで初期化しているから、ですね。
つまり、コレクションのメンバも、そのメンバも、大元のCCのコンストラクタ呼び出しに登場し、全体が定数であることが表現できているから。

従って、コレクションでも次のように使うとNGです。

class CC2 {
  final l;
  const CC2(x, y, z):this.l = const[x, y, z]; // コンパイル時エラー
}

なお、const[...]constは(メンバではなく)コレクション自体が定数(メンバの入れ替えもない)と宣言するためですね。
型推論してくれても良さそうですが。

どうすれば良いか?

これなら通ります。

class D2 {
  final C y;
  const D2(this.y);
}

var d2 = const D2(const C(1));

代替案 (言語仕様提案)

こんな代替案を考えてみました。

  1. 定数コンストラクタをnewで呼び出せないようにする
  2. 型推論でconst指定を暗黙的に遷移的に適用する
  3. 定数コンストラクタをnewで呼び出したら、そのメンバ初期化のconstnewに読み替えて実行する

1.はnewconstでコンストラクタを作り分ける・使い分けるのがメンドクセ。

class D {
  final C y;
  D.f(x):y = new C(x); // final版
  const D.c(x):y = const C(x); // const版
}

var d = new D.f(1);
const D = const D.c(2);

また、

In contrast, the legal examples make sense regardless of whether the constructor is invoked via const or via new.

は信念のようなので、さらに、後方互換性がなくなるので、変えるのは難しいと思います。

2.ならconst[...]constが不要になりそう。でも、ユーザ定義クラスの場合、その初期化において敢えてnewって書かせるのか? キモチワリ。

class D3 {
  final C y;
  final l;
  const D3.c(x, this.l):y = new C(x); // 定数コンストラクタなのにnew C(x)って...
}

var d3 = const D3(3, [true, false, ['a', 'b', 'c']]); // [...]の前のconstなんて不要でしょ? まあ、new...の構文糖衣だけどね。

3.じゃ何がいけないんだろうか?
newで呼び出せる時点で定数コンストラクタのconstは定数コンテキストで呼び出された場合 想定した事前チェックの指示でしか無いのだから、そのメンバ初期化におけるconstnewに読み替えて動的に実行したところで何の問題もないはずですよね?

class D {
  final C y;
  const D(x):y = const C(x); // コンパイル時はnewでの呼び出しも想定して new C(x) 呼び出しを展開
}

var d = new D(1); // 中でnew C(1)が呼び出される
const D = const D(2); // もちろん中ではconst C(2)が解釈される
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away