Dart
static
const
final

Dartの変数定義時の修飾static/final/const、そしてconst constructorについて

はじめに

Dart言語における変数定義時の修飾子にはfinal、const、およびstaticがある。
final,const及び型を指定しないことを意味する「var」を指定することもできる。
本記事では、これらの変数修飾子、およびconst constructorについて説明する。

final指定

finalが指定された変数は、プログラム開始後のある時点で一回だけ初期化され、初期化以降は、代入などを通じて変更されない/できないことが保証される(再代入不可)。なお、finalな変数が「指す先」のメモリ領域の内容が変更されることについての制約はない。

finalの使用例

  final int i = 0;
  i = 2; // Error(再代入不可)
  final List<int> a = [1,2,3];
  a[0] = 4; // OK(指す先は変更可能)

const指定

変数の値が「コンパイル時定数(compile-time constant)」であること、すなわち、コンパイル時に確定している値であることを示すための指定。constな変数の値はプログラムの実行開始に先立って初期化されており、プログラムの実行を通じて不変であることが保証される。const変数は再代入も不可である。つまり、finalの意味に加え、constな変数が指す先のメモリ領域の内容も変更不可であることを表す。

constの使用例

  const int i = 0;
  i = 2; // Error(再代入不可)
  const List<int> a = [1,2,3]; // NG(非const値)
  const List<int> b = const [1,2,3]; // OK(const値)
  b[0] = 4; // NG(指す先も変更不可)

コンパイル時定数

const指定された定数を含み、より一般的な概念が「コンパイル時定数(compile-time constant)1」である。以下はコンパイル時定数として扱うことのできる要素である。const指定する変数の値の初期化値はコンパイル時定数である必要がある。

  • 数値リテラル
    • 例: 1.0
    • 例: 3
  • 文字列リテラル
    • 例: "abc"
  • boolean型リテラル
    • 例: true, false
  • const指定された定数
    • 例: myConst
  • null値
    • 例: null
  • constなリストリテラル(型はListBase&&UnmodifiableListMixin)。当然ながら要素もコンパイル時定数である必要がある。
    • 例: const [1,2,3]
  • constなマップリテラル(型は_ImmutableMap)。当然ながら要素もコンパイル時定数である必要がある。
    • 例: const {"a":1, "b":2}
  • 数値型のコンパイル時定数の算術演算式(の結果の値)
    • 例: 2+3
  • 文字列型のコンパイル時定数の結合演算式(の結果の値)
    • 例: "abc"+"def"
  • コンパイル時定数の文字列テンプレート展開
    • 例: "const myConst ${myConst} is const"(訂正: 文字列テンプレートはconstにならない)
  • コンパイル時定数の演算(の結果の値)
    • 例: myConst1+myConst2+5
    • 例: myConst1 && myConst2
  • const constructor(後述)の呼び出し。const constructorに与える引数(finalフィールドに設定するための値)はコンパイル時定数である必要がある。
    • 例: const ClassWithConstantConstructor(1, "abc", [1,2,3])
  • その他単純な演算式
    • 例: 3 == 4 ? "a" : "b"
  • 関数や静的メソッドの参照
    • 例: import 'dart:math';const f = min;

以下はコンパイル時定数にはならない。

  • 乱数値など呼び出す度に変化する値や式
  • 入出力の副作用を伴う値
  • ランタイムにメモリ上にアロケートされる値(newで構築されるオブジェクトインスタンス)
  • 値の計算に静的関数やメソッドの呼び出し結果を必要とする値
    • たとえばconst [1,2,3].length
    • たとえばリストの結合やマップの併合
    • n回の繰り返し(たとえnがコンパイル定数であろうとも)。

コンパイル時定数の実行時モデルとしては、ROM領域もしくは「初期化された静的定数領域」に、領域内のポインタ参照構造を含めて配置されることを想定すれば良いと思われる。

全体としては、残念ながら皆が期待するところのコンパイル時プログラミングの用には供せないのではないかと思う(が定かではない)。

const constructor

前項の例で示したconst constructorの呼び出しは、例えば以下のクラス定義を前提としている。

class ClassWithConstantConstructor{
  final int n;
  final String s;
  final List l;
  const ClassWithConstantConstructor(this.n, this.s, this.l); // const constructor
}

上記のようにconst constructorはボディを持つことができない。
前項の例でも示したように、const constructorの呼び出しは通常のコンストラクタ呼び出しで使用するnewの代りにconstキーワードを使用する。

var x = const ClassWithConstantConstructor()

クラスには、const constructorと通常のconstructorの両方を定義することもできる。
Dart2ではインスタンス生成のためのキーワードnew、constが省略できるが、constの文脈ではconst constructorが、非constの文脈では通常のconstructorが呼び出される。
(訂正)クラスには、同一名のconst constructorと通常のconstructorの両方を定義することはできない(重複定義となり構文エラーとなる)。一方を名前付きコンストラクタにする、異なる名前の名前つきコンストラクタにするなどすれば、1つのクラスが通常のconstructorとconst constructorの両方を持つことができる。

const constructorを使うことで、「コンパイル時に定まるDAG構造2」のリテラルを表現することができる。
なお、構造をもったコンパイル時定数(DAG構造、リスト、マップを含め)一般に、これらの構造はinternされる。すなわち値や構造が一致していればオブジェクトインスタンスが一致している保証がある。

internされることは、コンパイル時定数がswitch-case文のcase節に利用できることに関連して重要である。

const constructorの例

class Cons {
  final dynamic car;
  final Cons cdr;
  const Cons(this.car, [this.cdr=null]);
  @override
  String toString() =>
     "(${this.car} . ${this.cdr == null ? "nil" : this.cdr.toString()})";

}

main() {
  const Cons list1 = Cons(1, Cons(2));
  const Cons list2 = Cons(list1, Cons(list1));
  const Cons list3 = Cons(Cons(1, Cons(2)), Cons(Cons(1, Cons(2))));

  print(list2); // ((1 . (2 . nil)) . ((1 . (2 . nil)) . nil))
  print(list3); // ((1 . (2 . nil)) . ((1 . (2 . nil)) . nil))
  print(list2 == list3); // 値が等しいのでtrueが表示される。
  print(identical(list2, list3)); // オブジェクトが等しいのでtrueが表示される(internされている)。
}

constとfinalの違い

  • finalは変数の性質なので、「final変数に代入できない」など、代入の左辺になったときの変数としての扱いについて差異が生じる。しかし、final変数の保持するには影響を与えない。たとえば、「〜には、final変数の値を代入できない(orできる)」「〜の場合の引数には、final変数の値を渡せない」といったfinal指定起因のに対する制約はない。
  • constは変数の性質でもあることに加えて、const変数が保持する値の性質も規定している(つまり値は「コンパイル時定数」であることを意味している)ので、const変数の値を他の変数に代入したり、関数の引数として値を渡したりするときに、constが付く・付かないで制約回避できる・できないといった差異が生じる場合がある。たとえば、以下の状況に使用できるのはコンパイル時定数のみである。
    • const変数に代入
    • 省略可能パラメタのデフォルト値
    • switch case文のcase節の式

static指定

クラスのフィールド変数に指定し、その変数がインスタンスごとに保持されるのではなく、クラスで1つの実体を持つことを宣言する。

なお、Dart言語では関数ローカルなstatic変数や、関数の外側でのstatic指定は文法上使用できず、クラススコープのstaticのみが文法上許されている。CやC++における、名前空間のコントロールのためのstaticや、C/C++/Javaにおける「可視範囲は関数ローカルだが生存期間はグローバルであることを意味するstatic」はDartでは使用できない。

staticの例

class Test {
  static int i1;
  static var i2;
  static final int i3 = 3;
  static const int i4 = 4;
}
static int i5;  // Error
main() {
  static int i6; // Error
}

(余談)Dartでの可視性の制御

前節で示したように、C/C++における「モジュールローカル」を意味するstaticの用法はDartには無い。Dartでのモジュール(ライブラリ)ローカルの指定は、大域変数の変数名をアンダースコア(下線, '_')で始めることで実現される。同様に、可視性をクラス内に限定するためにはフィールドやメソッド名をアンダースコアで開始する。

final int _modulePrivate = 0; // 他モジュールに公開されない

class _SomeClass { // 他モジュールに公開されないクラス
  int _num;  // クラス外から不可視なフィールド
}

変数の初期化タイミング

ローカル変数は、関数が呼びだされ、その変数が宣言されているブロックに処理が入った時点で値が初期値指定があれば、その値で初期化される。この事はfinal、const、varを通じて同じである。ただし、constなローカル変数の初期化式はコンパイル時定数であることが必要である。

初期化式を伴なって宣言されたconstではない大域変数、クラス変数(static変数)は、その変数の初回参照時に初期化式が評価され、値が確定する(Lazyな初期化)。constとは異なり、任意の初期化式が指定可能である。

初期化指定のある大域変数やクラス変数を初回参照する前に代入したとき、初期化式は評価されず、代入する値に設定される。このことは直感的ではないかもしれないので再度強調しておくと、初期化式を伴なって宣言された大域変数やstatic変数を「参照する前に代入する」と、初期化がスキップされる。初期化式が関数呼び出しを含むのであれば、それは実行されないということである。

表にまとめると

指定 変数への再代入 スコープ 変数値の設定タイミング 初期化/代入できる値 変数の指す先の破壊的変更
varもしくはなし 大域 初回参照時(Lazy)か代入時 任意
ローカル 初期化
クラス コンストラクタでの初期化時、代入時
final 不可 大域 初回参照時(Lazy) 任意
ローカル 初期化時
クラス コンストラクタでの初期化時
const 不可 大域 コンパイル時に確定 コンパイル時定数式 不可
ローカル
static var(あるいは単にstatic) クラス 初回参照時(Lazy)か代入時 任意
static final 不可 初回参照時(Lazy)
static const 不可 コンパイル時に確定 コンパイル時定数式 不可
  • スコープが「クラス」は、クラスのフィールドとして定義される変数の意。

  1. 「const値」でも通じると思われる。 

  2. 枝を共有できるので(次に例を示す)、ツリー以外のDAGも表現できる。なお、循環構造は表現できないようである(確信はないけど。もしできるのであれば教えて欲しい)。