const variableの用途

勉強会で忙しくて、すっかりブログのことを忘れていました。

6月は名古屋でDartに関する勉強会が2つもあり、両方とも発表者として参加しました。 資料はGoogle Driveに挙げておいたので、興味が有る方は見てやってください。

自分の持ってる情報の大体のところは資料の中に記載されていますが、勉強会で口頭でのみ説明したものは勿論、反映されておりません。ブログでは、それを補足していきたいと思います。

さて、今回はconstです。Dart独自の機構ではないので、面白みはありませんが。

まずはfinal variableとconst variableの違いから。

final variable

final variable(final変数)は、再代入ができない変数です。様々な言語でお馴染みですね。

final s1 = "AB";
// s1 = "BB" <- コンパイルエラー
print(s1);

const variable

const variable(const変数)は変数への再代入ができなくなる他、
コンパイル時に初期化され、そのconst variableを参照している場所にインライン展開されます。

以下のようにconst variableを定義した場合。

const s1 = "AB";
print(s1);

コンパイル時には以下のように、インライン展開されます。

print("AB");

どちらもいわゆる「定数」の定義をする目的で使われますが、
finalは実行時に変数の参照先を固定するのに対し、
constはコンパイル時に変数を展開することで、「定数」を実現しています。

const variableの制限

const variableはfinal variableのサブセット、というわけではありません。
final variableは、top levelに定義された場合にのみ、遅延評価の対象になります。1
constはまさにその逆で、コンパイル時に初期化式が評価されます。
そのため、const variableの初期化式や、変数に代入する値にはいくつかの制限があります。

  • リテラル値であること2
  • const付きListリテラル
  • const付きMapリテラル
  • constコンストラクタの呼び出し
  • top levelのconst定数、またはstaticなconst定数への参照
// リテラル値
const int INITIAL_VALUE = 0;
const String ADMIN = "zetta1985";
const bool RELOADABLE = false;
const NULL = null;

// const付きListリテラル
const List<String> SEASEN = const ["Spring", "Summer", "Autumn", "Wintter"];
// const付きMapリテラル
const Map<String, String> WEB_BROWSERS = const {
  "Google" : "Chrome",
  "Microsoft" : "Internet Explorer",
  "Mozilla" : "FireFox",
  "Apple" : "Safari"
};

// constコンストラクタの呼び出し
const AICHI = const Location("Aichi");

// top levelのconst定数、またはstaticなconst定数への参照
const USER = ADMIN;
const MY_COMPANY = Location.JAPAN;

class Location {
  static const Location JAPAN = const Location("Japan");
  final String name;
  const Location(this.name);
  String toString() => "Location:$name";
}

main () {
  print(INITIAL_VALUE);
  print(ADMIN);
  print(RELOADABLE);
  print(NULL);
  print(USER);
  print(MY_COMPANY);
  print(SEASEN);
  print(WEB_BROWSERS);
  print(AICHI);
}

ここで注意点をいくつか。

  • List, Mapには副作用を及ぼすメソッドが定義されているが、const初期化した場合は、そのようなメソッドを呼ぶと例外が発生する。3
  • constコンストラクタで生成したオブジェクトのフィールドは変更不可かつconst variableに代入可能である必要がある
  • constコンストラクタでは、フィールドの初期化や親クラスのconstコンストラクタ呼び出ししかできない4

constの用途

主に最適化のために使われそうですが、その性質を利用して、
constコンストラクタを持つクラスのインスタンスの不変性を保証できます。
また、同じ値で初期化した複数のconst variableの等値性5も保証出来ます。

class Location {
  final String name;    // final無しの場合は変更可能であるため、コンパイルエラーになる
  const Location(this.name);
}
  var l1 = const Location("Aichi");
  var l2 = const Location("Aichi");
  print(identical(l1, l2));  // true

しかし、constコンストラクタを定義したとしても、constを使わずに単にnewで初期化することもできます。
その場合、オブジェクトの不変性は保たれますが、等値性は失われます。

  var l1 = new Location("Aichi");   // newでも呼べる
  var l2 = new Location("Aichi");
  print(identical(l1, l2));  // false

よって、constコンストラクタを定義した場合は、外部からconst初期化されることを想定しない方が良いでしょう。
個人的には、constコンストラクタを持つクラスのインスンタンス生成を外部で行わせたい場合は、
constコンストラクタをlibrary private6にし、別途外部初期化用のコンストラクタを提供したほうが良いと思います。

また、リテラル値などはこの限りではありませんが、その場合でも等値性が保たれていることを前提にすべきではないでしょう。

まとめ

  • const variableを外部に参照させる場合は、等値性が保たれることを前提にしない
  • 単純な状態不変のValue Objectを定義するためにconstコンストラクタが便利(コンパイラが不変性を保証)
  • constコンストラクタを外部に公開した場合に、const初期化されることを想定すべきではない

  1. top-levelであれば、varでも遅延評価の対象になります。 

  2. number, boolean, string, null 

  3. const付きの場合、List/Mapの実装が変更不可なList/Mapになります。 

  4. 入力検証などの他のロジックを実行させることはできません。 

  5. コード内の‘identical'は引数のオブジェクトが等値である場合にtrueを返す関数 

  6. Dartでは_(アンダースコア)を識別子の前に付けると、同じlibrary内でしか参照できなくなります。 

このエントリーをはてなブックマークに追加
comments powered by Disqus