LoginSignup
3

More than 5 years have passed since last update.

static thisについてちゃんと理解していますか?

Last updated at Posted at 2016-07-14

static this と shared static thisの説明

D言語にはstatic constructor(日本語で言うと静的コンストラクタ)という言語機能が存在します。(以下の説明ではstatic thisstatic constructorを同義とします。)
名前から想像がつくようにstaticなコンストラクタで、すなわち、main関数が実行されるよりも前に実行されるコンストラクタで、主にモジュールやクラスを初期化するために使います。
(なお、説明ではコンストラクタのみを説明していますが、デストラクタについても同様です。詳しくは言語仕様を参照してください。)

シングルトンパターンでクラスを書く場合などはstatic thisを使うと便利だと思います。(僕は殆どシングルトンパターンでプログラムを書く(/書いた)ことはないので、次に示す例が理想的なシングルトンパターンであるかどうかはわかりません。)
適当な例としては次のようなコードです。

singleton.d
final class Singleton {
  // 唯一のインスタンス。
  private static Singleton singleton;
  private this() {}

  // main関数よりも先に呼び出され、singletonにインスタンスを代入する。(Singletonクラスを静的に初期化する)
  static this() {
    singleton = new Singleton;
  }

  @property static typeof(this) getInstance() {
    return singleton;
  }
}
main.d
/**
 * Compile: $ dmd main.d singleton.d
 */

import singleton;

void main() {
  // this()をprivateにしてあるので、次のようにしてインスタンスが作られることはない。
  // Singleton singleton = new Singleton();
  Singleton singleton = Singleton.getInstance;
}

このように使う限りでは、static thisについてこれ以上の理解は必要が無いように思われます。

さて、先ほどstatic thisはmain関数よりも先に実行されると書きました。
それを確かめてみましょう。

import std.stdio;

int[] ar;

static this() {
  ar ~= 0;
}

void main() {
  ar ~= 1;

  assert (ar == [0, 1]);
}

assertionは成功します。
では、複数のstatic this同一module内に複数あった場合どのようになるのでしょうか。
これは、D言語の言語仕様のModulesのページのStatic Construction and Destructionにも書いてあるとおり、ソースコードに書かれた順に実行されます。

There can be multiple static constructors and static destructors within one module. The static constructors are run in lexical order, the static destructors are run in reverse lexical order.

(run in lexical orderが相当します)

実験してみても良いでしょう:

import std.stdio;

static this() {
  writeln("First");
}

static this() {
  writeln("Second");
}

void main() {
  writeln("Main");
}

/**
 * Output:
 *   First
 *   Sencond
 *   Main
 */

また、static thisの仲間にshared static thisというものも有ります。
両者の違いは、static thisがスレッドローカルなメモリ領域で動くのに対し、shared static thisはグローバルなメモリ領域で動くという違いがあります。
簡単にいえば、static thisはスレッドを作るたびに呼ばれますが、shared static thisはプログラムが起動された時に一度実行されるだけです。
これも簡単に実験ができます。

import std.stdio;
import core.thread;

static this() {
  writeln("static this was called");
}

shared static this() {
  writeln("shared static this was called");
}

void main() {
  foreach(_; 0..2) {
    new Thread({}).start;
    new Thread({}).start;
  }
}

/**
 * Output:
 *   shared static this was called
 *   static this was called
 *   static this was called
 *   static this was called
 */

ちゃんと3回、static this was calledが出力されていることから、スレッド作成時にstatic thisが呼ばれていることが確認できますね。
また、出力には違和感があるかもしれませんね。というのも、static thisのほうがshared static thisよりも先に書かれているのに、実行順序が逆になっています。
これは、shared static thisのほうがstatic thisよりも先に呼ばれるようになっているからです。もちろん、shared static thisを複数書いた場合は、書かれた順番に(ソースコード的に上から)実行されます。(その後にstatic thisが呼ばれる。)
なお、デストラクタの場合はコンストラクタと実行される順番が異なり、書かれた順番とは逆の順番で実行されます。
a, bをコンストラクタ, c, dをデストラクタとしてa -> b -> c -> dという順番でコードを書いた場合、呼ばれる順番は
a -> b -> d -> c
となります。
また、静的デストラクタのうち、shared static ~thisはコンストラクタとデストラクタの実行順序の関係と同様に、コンストラクタとは逆の順番で実行されます。つまり、一番最後にshared static ~thisは実行されます。

さて、static thisについての説明は次で終わります。

今までは、同一モジュール内(単一ファイル内)に複数のstatic thisを置いた場合を考えてきました。次は、複数のモジュール(複数ファイル)を用いた場合について見てみましょう。

初めにソースコードから:

ma.d
import std.stdio;
static this() {
  writeln("static this[ma] was called");
}
mm.d
/**
 * Compile: $ dmd mm.d ma.d
 */

import std.stdio;

static this() {
  writeln("static this[mm] was called");
}

import ma;

void main() {}

このように考えた時、単純に考えるとstatic thisの次にimport ma;でmaモジュール(D言語では明示的にmodule名を書かないかぎり、ファイル名がそのままモジュール名になってくれます)をimportしているので

static this[mm] was called
static this[ma] was called

と出力される、と考えてしまうでしょう。

しかし、実際には

static this[ma] was called
static this[mm] was called

と出力されます。
これは、先ほどの言語仕様のOrder of Static Constructionにも書いてある正しい挙動です。

The order of static initialization is implicitly determined by the import declarations in each module. Each module is assumed to depend on any imported modules being statically constructed first. Other than following that rule, there is no imposed order on executing the module static constructors.

Each module is assumed to depend on any imported modules being statically constructed first.が該当します。
すなわち、importしている側のコードはimportしているすべてのモジュールに依存していると推測され、最初にすべて解決(static constructorが呼ばれる)されます。

それゆえ、セマンティクス的にはmm -> maという順番で出力されるように思えますが、実際にはma -> mmという順番で出力されます。

とりあえず、ここまでがstatic thisshared static thisについての説明です。

ちなみに、さらっと最初のコードで、classの中にstatic thisを書きましたが、そのようにclassの中とstructの中にもstatic thisを書くことが出来ます。

さて、どうするとまずいか

これも言語仕様に書いてあるのですが(引用及びリンクは後で貼ります)、具体的には以下のコードは実行時にランタイム例外が発生します。

a.d
import b;

static this() {}
b.d
import a;

static this() {}
main.d
/**
 * Compile: $ dmd main.d a.d b.d
 */
import a;

void main() {}

これを実行すると、前述のとおりランタイム例外が発生します。
出力されるエラーは以下:

object.Exception@src/rt/minfo.d(167): Aborting: Cycle detected between modules with ctors/dtors:
a* ->
b* ->
a

で、これが何を意味しているのかというと、「循環が発生しているよ!!」 ということです(そのままですが)。

言語仕様でもこの挙動は定義されています(Order of Static Construction):

Cycles (circular dependencies) in the import declarations are allowed as long as not both of the modules contain static constructors or static destructors. Violation of this rule will result in a runtime exception.

ようするに、循環するようなimport(つまり、aがbをimport, bがaをimportしている状態。上に書いたコードと構成は同じ状況)は、それらがともに静的コンストラクタを持たないかぎりは許容されるが(aのみが静的コンストラクタを持つという場合は問題ない)、そうでない場合、すなわち、(上のサンプルコードのように)両方のモジュールが互いにそれぞれ静的コンストラクタを保つ場合は実行時に例外が発生するということです。

上の問題の解決策はいくつかあるとは思いますが、一つの解決策としては次のようなものがあると思います:

a.d
import b;

//static thisによる初期化をやめ、staticな初期化用の関数を定義
private static bool initialized;

static initialize_A() {
  if (!initialized) {
    import std.stdio;

    writeln("initialize_A");
    initialized = true;
  }
}

// 初期化してあるかどうか
static bool isInizializedA() {
  return initialized;
}
b.d
import a;

// aと同様
private static bool initialized;

static initialize_B() {
  if (!initialized) {
    import std.stdio;

    writeln("initialize_B");
    initialized = true;
  }
}

// 初期化してあるかどうか
static bool isInizialized_B() {
  return initialized;
}
c.d
//aとbを初期化するためのモジュール
import a;
import b;

//ここで初期化をする
static this() {
  initialize_A;
  initialize_B;
}

//初期化できてるかどうかの確認
static checkInitialized() {
  import std.stdio;

  writeln("a -> ", isInizialized_A);
  writeln("b -> ", isInizialized_B);
}
main.d
import c;

void main() {
  //cを経由してa, bが初期化できているかを確認する。
  checkInitialized;

  //こちら側にも反映されているかどうかをテスト
  import a;
  import b;
  import std.stdio;
  writeln("a -> ", isInizialized_A);
  writeln("b -> ", isInizialized_B);
}

このように書けば、importの循環を回避することが出来ます。

なぜこの記事を書いたか

僕が言語仕様を読んでなくて勝手にimportの循環に陥って軽くハマりかけたので、書くことにしました。

結論: 言語仕様を読みましょう。

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3