#アップデート (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);
}
4年の歳月を経てようやくこれが実現されたのは、Dartがバージョン2でコンパイルを重視した型安全な処理系に変わり、静的に深い解析を行うようになったのが要因だと思います。
とはいえ、まだDartPadでは変なエラーメッセージを出したり、コメントの有る行をconst D(x):y = const C(x);
と書いても通るなど、まだ動きが怪しいですが。
#まえおき
もちろん出来ますが、一見普通なのに通らないケースもあります。
まずは予備知識から。
static, final, constの違い
Dartではstatic
とfinal
とconst
はそれぞれ異なる意味を持っています。 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));
#代替案 (言語仕様提案)
こんな代替案を考えてみました。
- 定数コンストラクタを
new
で呼び出せないようにする - 型推論で
const
指定を暗黙的に遷移的に適用する - 定数コンストラクタを
new
で呼び出したら、そのメンバ初期化のconst
をnew
に読み替えて実行する
1.はnew
とconst
でコンストラクタを作り分ける・使い分けるのがメンドクセ。
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
は定数コンテキストで呼び出された場合 も 想定した事前チェックの指示でしか無いのだから、そのメンバ初期化におけるconst
もnew
に読み替えて動的に実行したところで何の問題もないはずですよね?
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)が解釈される