白猫のメモ帳

JavaとかC#とかJavaScriptとかHTMLとか機械学習とか。

staticイニシャライザとstaticコンストラクタは同じだと思っていました

こんにちは。

年の瀬ですね。
お仕事は納まりましたか。
あれ・・・なんかこれ去年も書いた気がする。

そもそもイニシャライザとかコンストラクタってなんだっけ

Java

staticイニシャライザはJavaの機能。

class Hoge {
    
    static String fuga;
    
    // staticイニシャライザ
    static {
        System.out.println("static initializer");
    }
    
    // イニシャライザ
    {
        System.out.println("initializer");
    }

    // コンストラクタ
    Hoge() {
        System.out.println("constructor ");
    }
}

Javaにはコンストラクタとは別にイニシャライザという機能があって、
文字通りイニシャライズ(初期化)する時に呼ばれます。

staticイニシャライザはクラスにstaticアクセスしたときに呼ばれるため、
staticフィールドにアクセスしたり、インスタンス生成すると呼ばれます。

一方で、staticでないイニシャライザとコンストラクタはクラスのインスタンスを生成したときに呼ばれます。

// static initializer
String fuga = Hoge.fuga;

// static initializer
// initializer
// constructor 
new Hoge();

C#

で、staticコンストラクタはC#の機能。

class Hoge
{

    internal static string fuga;

    // staticコンストラクタ
    static Hoge()
    {
        Console.WriteLine("static constructor");
    }

    // コンストラクタ
    internal Hoge()
    {
        Console.WriteLine("constructor");
    }
}

C#にはイニシャライザという機能はなくて、
クラスにアクセスした際に処理を実行したい場合はstaticコンストラクタを使います。

// static constructor
var fuga = Hoge.fuga;

// static constructor
// constructor
new Hoge();

ここまでの感じでは同じように使っていいように思えます。

継承してみる

継承のあるパターンを確認してみます。

Java

class Parent {
    
    static String hoge;
    
    static {
        System.out.println("Parent static initializer");
    }
    
    {
        System.out.println("Parent initializer");
    }

    Parent() {
        System.out.println("Parent constructor ");
    }
}

class Child extends Parent {
    
    static String fuga;
    
    static {
        System.out.println("Child static initializer");
    }
    
    {
        System.out.println("Child initializer");
    }

    Child() {
        System.out.println("Child constructor ");
    }
}

子クラスから親クラスのstaticフィールドにアクセスすると、親クラスのstaticイニシャライザだけが呼ばれます。

// Parent static initializer
String hoge = Child.hoge;

子クラスのstaticフィールドにアクセスすると、親クラスと子クラスのstaticイニシャライザが呼ばれます。

// Parent static initializer
// Child static initializer
String fuga = Child.fuga;

親クラスのインスタンスを生成すると、親クラスのstaticイニシャライザ、イニシャライザ、コンストラクタが呼ばれます。

// Parent static initializer
// Parent initializer
// Parent constructor 
new Parent();

子クラスのインスタンスを生成すると、全部呼ばれます。順番は見たとおり。

// Parent static initializer
// Child static initializer
// Parent initializer
// Parent constructor 
// Child initializer
// Child constructor 
new Child();

C#

class Parent
{

    internal static string hoge;

    static Parent()
    {
        Console.WriteLine("Parent static initializer");
    }

    internal Parent()
    {
        Console.WriteLine("Parent constructor ");
    }
}

class Child : Parent
{

    internal static string fuga;

    static Child()
    {
        Console.WriteLine("Child static initializer");
    }

    internal Child()
    {
        Console.WriteLine("Child constructor ");
    }
}

子クラスから親クラスのstaticフィールドにアクセスすると、親クラスのstaticコンストラクタだけが呼ばれます。
ここは一緒。

// Parent static constructor
var hoge = Child.hoge;

子クラスのstaticフィールドにアクセスすると、子クラスのstaticコンストラクタだけが呼ばれます。
あれ、なんか挙動が違うぞ・・・。

// Child static constructor
var fuga = Child.fuga;

親クラスのインスタンスを生成すると、親クラスのstaticコンストラクタ、コンストラクタが呼ばれます。
ここは一緒。

// Parent static constructor
// Parent constructor
new Parent();

子クラスのインスタンスを生成すると、全部呼ばれます。
が、なんか順番が違います。
staticコンストラクタは子が先で、コンストラクタは親が先。ややこしい…。

// Child static constructor
// Parent static constructor
// Parent constructor
// Child constructor
new Child();

深くは掘り下げず、そういうもんだと思ってほかのパターンも見ていきます。

クラスジェネリクスをつけてみる

なんかずいぶん長くなってきてしまったので、いらないとこ除外しますね。

Java

class Parent<T> {
    
    static {
        System.out.println("Parent static initializer");
    }

    Parent() {
        System.out.println("Parent constructor ");
    }
}

class Child1 extends Parent<String> {
    
    static {
        System.out.println("Child1 static initializer");
    }

    Child1() {
        System.out.println("Child1 constructor ");
    }
}

class Child2 extends Parent<Integer> {
    
    static {
        System.out.println("Child2 static initializer");
    }

    Child2() {
        System.out.println("Child2 constructor ");
    }
}

Child1とChild2を連続してインスタンス生成すると、Parentのstaticイニシャライザは1回しか呼ばれません。
これは型削除(イレイジャ)という機能によって、コンパイル時にジェネリクスが消されているからでしょう。

// Parent static initializer
// Child1 static initializer
// Parent constructor 
// Child1 constructor 
new Child1();

// Child2 static initializer
// Parent constructor 
// Child2 constructor 
new Child2();

C#

class Parent<T>
{
    static Parent()
    {
        Console.WriteLine("Parent static constructor");
    }

    internal Parent()
    {
        Console.WriteLine("Parent constructor ");
    }
}

class Child1 : Parent<string>
{
    static Child1()
    {
        Console.WriteLine("Child1 static constructor");
    }

    internal Child1()
    {
        Console.WriteLine("Child1 constructor ");
    }
}

class Child2 : Parent<int>
{
    static Child2()
    {
        Console.WriteLine("Child2 static constructor");
    }

    internal Child2()
    {
        Console.WriteLine("Child2 constructor ");
    }
}

Child1とChild2を連続してインスタンス生成すると、Parentのstaticイニシャライザが2回呼ばれます。
C#では型削除は行われないので(内部実装的にはプリミティブは展開して、それ以外は共有している?)、別のクラスとして扱われるのでしょう。

これは別にstaticイニシャライザ/コンストラクタの機能の違いというわけではなく、
あくまで副次的なものだと思いますが、なかなかにややこしいです。

おまけ 自身のインスタンスをstaticに持つ場合

これはJavaC#も挙動は同じなのですが、シングルトンパターンなんかでよくやる、
自分自身のインスタンスをstaticで保持する場合の呼び出し順が難しいのでついでに見ておきます。

class Parent {
    
    static {
        System.out.println("Parent static initializer");
    }

    Parent() {
        System.out.println("Parent constructor ");
    }
}

class Child extends Parent {
    
    static Child singleton = new Child();
    
    static {
        System.out.println("Child static initializer");
    }

    private Child() {
        System.out.println("Child constructor ");
    }
}

なんでstaticがあとにくるんじゃーい。

// Parent static initializer
// Parent constructor 
// Child constructor 
// Child static initializer
Child child = Child.singleton;

ほかにも違いがあるんですかね

普通にコードを書いている上ではそんなに出会うパターンではないとは思うのですが、
なんだかこの辺りって難しいですよね。

誰にでもわかりやすいコードを書けるようにしたいものです。