コンストラクターとファイナライザー

コンストラクターは、そのクラスのインスタンスが作成されるときに呼び出されるクラスのメソッドです。彼らの主な責任は、新しいオブジェクトを有用で一貫した状態にすることです。

デストラクタ/ファイナライザは、クラスのインスタンスが破棄されたときに呼び出されるクラスのメソッドです。 C# では、明示的に記述/使用されることはめったにありません。

# 静的コンストラクター

静的コンストラクターは、型のメンバーが最初に初期化されるとき、静的クラス メンバーが呼び出されるとき、または静的メソッドが呼び出されるときに呼び出されます。静的コンストラクターはスレッド セーフです。静的コンストラクターは一般的に次の目的で使用されます。

  • 同じクラスの異なるインスタンス間で共有される状態である静的状態を初期化します。
  • シングルトンを作成する

例:

class Animal
{
    // * A static constructor is executed only once,
    //   when a class is first accessed.
    // * A static constructor cannot have any access modifiers
    // * A static constructor cannot have any parameters
    static Animal()
    {
        Console.WriteLine("Animal initialized");
    }

    // Instance constructor, this is executed every time the class is created
    public Animal()
    {
        Console.WriteLine("Animal created");
    }

    public static void Yawn()
    {
        Console.WriteLine("Yawn!");
    }
}

var turtle = new Animal();
var giraffe = new Animal();

出力:

動物の初期化
作成した動物
作成された動物

デモを見る

最初の呼び出しが静的メソッドに対するものである場合、静的コンストラクターはインスタンス コンストラクターなしで呼び出されます。とにかく、静的メソッドはインスタンスの状態にアクセスできないため、これで問題ありません。

Animal.Yawn();

これは以下を出力します:

動物の初期化
あくび!

静的コンストラクターの例外および汎用静的コンストラクターも参照してください。

シングルトンの例:

public class SessionManager
{
    public static SessionManager Instance;

    static SessionManager()
    {
        Instance = new SessionManager();
    }
}

# シングルトン コンストラクタ パターン

public class SingletonClass
{
    public static SingletonClass Instance { get; } = new SingletonClass();

    private SingletonClass()
    {
        // Put custom constructor code here
    }    
}

コンストラクターはプライベートであるため、 SingletonClass の新しいインスタンスはありません コードを使用して作成できます。 SingletonClass の単一インスタンスにアクセスする唯一の方法 静的プロパティ SingletonClass.Instance を使用しています .

Instance プロパティは、C# コンパイラが生成する静的コンストラクターによって割り当てられます。 .NET ランタイムは、静的コンストラクターが最大 1 回実行され、Instance より前に実行されることを保証します。 が最初に読まれます。したがって、すべての同期と初期化の問題はランタイムによって実行されます。

静的コンストラクターが失敗した場合、Singleton が失敗することに注意してください。 クラスは、AppDomain の存続期間中、永久に使用できなくなります。

また、静的コンストラクタは Instance の最初のアクセス時に実行される保証はありません。 .むしろ、その前のある時点で実行されます .これにより、初期化が発生する時間が非決定的になります。実際のケースでは、JIT は コンパイル 中に静的コンストラクターを呼び出すことがよくあります。 Instance を参照するメソッドの (実行ではない) .これはパフォーマンスの最適化です。

シングルトン パターンを実装する他の方法については、シングルトンの実装ページを参照してください。

# デフォルト コンストラクタ

コンストラクタなしで型が定義されている場合:

public class Animal
{
}

次に、コンパイラは次と同等のデフォルトのコンストラクタを生成します:

public class Animal
{
    public Animal() {}
}

型のコンストラクターを定義すると、デフォルトのコンストラクターの生成が抑制されます。タイプが次のように定義されている場合:

public class Animal
{
    public Animal(string name) {}
}

次に Animal 宣言されたコンストラクターを呼び出すことによってのみ作成できます。

// This is valid
var myAnimal = new Animal("Fluffy");
// This fails to compile
var unnamedAnimal = new Animal();

2 番目の例では、コンパイラはエラー メッセージを表示します:

'Animal' には引数を 0 個取るコンストラクタが含まれていません

パラメーターなしのコンストラクターとパラメーターを受け取るコンストラクターの両方をクラスに持たせたい場合は、両方のコンストラクターを明示的に実装することで実現できます。

public class Animal
{
    
    public Animal() {} //Equivalent to a default constructor.
    public Animal(string name) {}
}

クラスがパラメーターなしのコンストラクターを持たない別のクラスを拡張する場合、コンパイラーはデフォルトのコンストラクターを生成できません。たとえば、クラス Creature がある場合 :

public class Creature
{
    public Creature(Genus genus) {}
}

次に Animal class Animal : Creature {} として定義 コンパイルされません。

# 強制的に静的コンストラクターを呼び出す

静的コンストラクターは常に型を最初に使用する前に呼び出されますが、それらを強制的に呼び出して RuntimeHelpers を使用できると便利な場合があります。 クラスはそのヘルパーを提供します:

using System.Runtime.CompilerServices;    
// ...
RuntimeHelpers.RunClassConstructor(typeof(Foo).TypeHandle);

備考 : コンストラクター自体だけでなく、すべての静的初期化 (フィールド初期化子など) が実行されます。

考えられる用途 : UI アプリケーションのスプラッシュ スクリーン中に初期化を強制するか、単体テストで静的コンストラクターが失敗しないようにします。

# 別のコンストラクターからコンストラクターを呼び出す

public class Animal
{
    public string Name { get; set; }

    public Animal() : this("Dog")
    {
    }

    public Animal(string name)
    {
        Name = name;
    }
}

var dog = new Animal();      // dog.Name will be set to "Dog" by default.
var cat = new Animal("Cat"); // cat.Name is "Cat", the empty constructor is not called.

# 基本クラス コンストラクターの呼び出し

基本クラスのコンストラクターは、派生クラスのコンストラクターが実行される前に呼び出されます。たとえば、Mammal の場合 Animal を拡張 、次に Animal のコンストラクターに含まれるコード Mammal のインスタンスを作成するときに最初に呼び出されます .

派生クラスが、基本クラスのどのコンストラクターを呼び出す必要があるかを明示的に指定していない場合、コンパイラーはパラメーターなしのコンストラクターを想定します。

public class Animal
{
    public Animal() { Console.WriteLine("An unknown animal gets born."); }
    public Animal(string name) { Console.WriteLine(name + " gets born"); }
}

public class Mammal : Animal
{
    public Mammal(string name)
    {
        Console.WriteLine(name + " is a mammal.");
    }
}

この場合、Mammal をインスタンス化します。 new Mammal("George the Cat") を呼び出して 印刷します

未知の動物が生まれる。
猫のジョージは哺乳類です。

デモを見る

基本クラスの別のコンストラクターを呼び出すには、 : base(args) を配置します。 コンストラクターの署名とその本体の間:

public class Mammal : Animal
{
    public Mammal(string name) : base(name)
    {
        Console.WriteLine(name + " is a mammal.");
    }
}

new Mammal("George the Cat") を呼び出す 印刷されます:

ジョージ・ザ・キャットが生まれる。
猫のジョージは哺乳類です。

デモを見る

# 派生クラスのファイナライザー

オブジェクト グラフが完成すると、順序は作成の逆になります。例えば。次のコードが示すように、スーパータイプはベースタイプの前にファイナライズされます:

class TheBaseClass
{
    ~TheBaseClass() 
    {
        Console.WriteLine("Base class finalized!");
    }
}

class TheDerivedClass : TheBaseClass
{
    ~TheDerivedClass() 
    {
        Console.WriteLine("Derived class finalized!");
    }
}

//Don't assign to a variable
//to make the object unreachable
new TheDerivedClass();

//Just to make the example work;
//this is otherwise NOT recommended!
GC.Collect();

//Derived class finalized!
//Base class finalized!

# 静的コンストラクターの例外

静的コンストラクターが例外をスローした場合、再試行されることはありません。この型は、AppDomain の存続期間中は使用できません。この型をさらに使用すると、TypeInitializationException が発生します。 元の例外をラップします。

public class Animal
{
    static Animal()
    {
        Console.WriteLine("Static ctor");
        throw new Exception();
    }

    public static void Yawn() {}
}

try
{
    Animal.Yawn();
}
catch (Exception e)
{
    Console.WriteLine(e.ToString());
}

try
{
    Animal.Yawn();
}
catch (Exception e)
{
    Console.WriteLine(e.ToString());
}

これは以下を出力します:

静的なctor

System.TypeInitializationException:'Animal' の型初期化子が例外をスローしました。 ---> System.Exception:タイプ 'System.Exception' の例外がスローされました。

[...]

System.TypeInitializationException:'Animal' の型初期化子が例外をスローしました。 ---> System.Exception:タイプ「System.Exception」の例外がスローされました。

実際のコンストラクターは 1 回だけ実行され、例外は再利用されることがわかります。

# コンストラクターでの仮想メソッドの呼び出し

C# の C++ とは異なり、クラス コンストラクターから仮想メソッドを呼び出すことができます (OK、C++ でも可能ですが、最初の動作は驚くべきものです)。例:

abstract class Base
{
    protected Base()
    {
        _obj = CreateAnother();
    }

    protected virtual AnotherBase CreateAnother()
    {
        return new AnotherBase();
    }

    private readonly AnotherBase _obj;
}

sealed class Derived : Base
{
    public Derived() { }

    protected override AnotherBase CreateAnother()
    {
        return new AnotherDerived();
    }
}

var test = new Derived();
// test._obj is AnotherDerived

C++ のバックグラウンドを持っている場合、これは驚くべきことです。基本クラスのコンストラクターは、既に派生クラスの仮想メソッド テーブルを認識しています!

注意 :派生クラスはまだ完全に初期化されていない可能性があり (そのコンストラクターは基本クラスのコンストラクターの後に実行されます)、この手法は危険です (これには StyleCop の警告もあります)。通常、これは悪い習慣と見なされます。

# ジェネリック静的コンストラクター

静的コンストラクターが宣言されている型がジェネリックである場合、静的コンストラクターは、ジェネリック引数の一意の組み合わせごとに 1 回呼び出されます。

class Animal<T>
{
    static Animal()
    {
        Console.WriteLine(typeof(T).FullName);
    }

    public static void Yawn() { }
}

Animal<Object>.Yawn();
Animal<String>.Yawn();

これは以下を出力します:

System.Object
System.String

ジェネリック型の静的コンストラクターはどのように機能しますか? も参照してください。

# コンストラクターとプロパティの初期化

プロパティ値の代入はに実行されますか? または クラスのコンストラクタ?

public class TestClass 
{
    public int TestProperty { get; set; } = 2;
    
    public TestClass() 
    {
        if (TestProperty == 1) 
        {
            Console.WriteLine("Shall this be executed?");
        }

        if (TestProperty == 2) 
        {
            Console.WriteLine("Or shall this be executed");
        }
    }
}

var testInstance = new TestClass() { TestProperty = 1 };

上記の例では、TestProperty 値は 1 です クラスのコンストラクター内ですか、それともクラス コンストラクターの後ですか?

このようにインスタンス作成でプロパティ値を割り当てる:

var testInstance = new TestClass() {TestProperty = 1};

に実行されます コンストラクターが実行されます。ただし、C# 6.0 でクラスのプロパティのプロパティ値を次のように初期化します:

public class TestClass 
{
    public int TestProperty { get; set; } = 2;

    public TestClass() 
    {
    }
}

に行われます コンストラクターが実行されます。

上記の 2 つの概念を 1 つの例に組み合わせる:

public class TestClass 
{
    public int TestProperty { get; set; } = 2;
    
    public TestClass() 
    {
        if (TestProperty == 1) 
        {
            Console.WriteLine("Shall this be executed?");
        }

        if (TestProperty == 2) 
        {
            Console.WriteLine("Or shall this be executed");
        }
    }
}

static void Main(string[] args) 
{
    var testInstance = new TestClass() { TestProperty = 1 };
    Console.WriteLine(testInstance.TestProperty); //resulting in 1
}

最終結果:

"Or shall this be executed"
"1"

説明:

TestProperty 値は最初に 2 として割り当てられます 、次に TestClass コンストラクターが実行され、

が出力されます
"Or shall this be executed"

そして TestProperty 1 として割り当てられます new TestClass() { TestProperty = 1 } による 、 TestProperty の最終値を作成します Console.WriteLine(testInstance.TestProperty) で印刷

"1"

# コメント

C# には実際にはデストラクタはありませんが、C++ スタイルのデストラクタ構文を使用するファイナライザがあります。デストラクタを指定すると、Object.Finalize() がオーバーライドされます 直接呼び出せないメソッド。

同様の構文を持つ他の言語とは異なり、これらのメソッドは違います。 オブジェクトが範囲外になると呼び出されますが、特定の条件下で発生するガベージ コレクターの実行時に呼び出されます。そのため、そうではありません 特定の順序で実行されることが保証されています。

ファイナライザーは、管理されていないリソースのクリーンアップを担当する必要がありますのみ (Marshal クラスを介して取得したポインター、p/Invoke (システム コール) を介して受信したポインター、または unsafe ブロック内で使用される生のポインター)。マネージド リソースをクリーンアップするには、IDisposable、Dispose パターン、および using を確認してください。

(参考資料:デストラクタはいつ作成する必要がありますか?)