静的コンストラクター(静的コンストラクタとも表記されます)は、クラスが初めて使われるときにランタイムが最大1回だけ呼び出す初期化処理です。静的データを初期化するほか、その型について一度だけ実行したい処理を置く場所としても使われます。new で明示的に呼ぶインスタンスコンストラクターと違い、いつ実行されるかを開発者が指定できません。この「呼ぶタイミングを握れない」性質が、初期化順序の取り違え、型が丸ごと使えなくなる例外、そしてデッドロックの原因になります。ここではC#の静的コンストラクターについて、公式仕様が定める実行タイミングと初期化順序、静的フィールド初期化子との関係、そして静的コンストラクターを書かないほうがよい場面までを整理します。
まとめ
- 静的コンストラクターは、最初のインスタンス生成の前、またはそのクラスで宣言された静的メンバーが参照される前に実行されます。プログラム開始時にまとめて動くわけではありません。
- 同じクラス内では、静的フィールド初期化子がクラス宣言のテキスト順に実行され、静的コンストラクターの直前に完了します。ただし階層内の静的コンストラクター同士の実行順序は、C#では仕様上「指定されていません」。
- 静的コンストラクターが例外を投げると、その型はアプリケーションドメインの有効期間中ずっと初期化されないままになり、以降のアクセスは
TypeInitializationExceptionになります。ランタイムが再実行することはありません。 - 静的コンストラクターを書くと型に
beforefieldinitが付かなくなり、ランタイムの最適化が制限されます。静的フィールドを初期値で埋めるだけなら、宣言時初期化にすべきです。 - 静的コンストラクターの本体でスレッドをブロックしてはいけません。タスクやロックの待機はデッドロックを招きえます。
以下では、この5点の根拠となる仕様と、実装で判断が分かれる場面を順に見ていきます。
静的コンストラクターの定義とインスタンスコンストラクターとの違い
コンストラクターは、型を使える状態に整えるための初期化処理です。このうち静的コンストラクターは、インスタンスではなく型そのものを初期化します。静的フィールドに実行時にしか決まらない値を入れる場合や、その型について一度だけ実行したい処理がある場合に使います。逆に言えば、インスタンスごとに変わる値を扱う余地はありません。
静的コンストラクターの宣言構文と仕様上の制約
public class Bus
{
// static readonly は宣言時か静的コンストラクター内でのみ代入できる
protected static readonly DateTime globalStartTime;
// 静的コンストラクター。アクセス修飾子もパラメーターも書けない
static Bus()
{
globalStartTime = DateTime.Now;
Console.WriteLine("静的コンストラクターが実行されました");
}
// インスタンスコンストラクター
public Bus(int routeNum)
{
RouteNumber = routeNum;
}
protected int RouteNumber { get; set; }
}
static に続けて型名を書き、丸括弧の中は空にします。ここに public などのアクセス修飾子やパラメーターを付けるとコンパイルエラーです。定義できるのはクラスまたは構造体につき1つだけで、継承もオーバーロードもできません。呼び出すのは共通言語ランタイム(CLR)であり、コードから直接呼ぶ手段はありません。静的コンストラクターを書かなかった場合、静的フィールドはC#の型ごとの既定値に初期化されます。
宣言・呼び出し・実行回数で見た7項目の差分
C#言語仕様の静的コンストラクターの節(Static constructors)が定める制約を、インスタンスコンストラクターと並べると次のようになります。
| 観点 | 静的コンストラクター | インスタンスコンストラクター |
|---|---|---|
| 宣言 | static Bus() | public Bus(int routeNum) |
| アクセス修飾子 | 付けられない | 付けられる |
| パラメーター | 取れない | 取れる |
| 定義できる数 | 型ごとに1つ | オーバーロード可 |
| 呼び出し | CLRが自動で呼ぶ | newで明示的に呼ぶ |
| 実行回数 | 最大1回 | インスタンスごと |
| 継承・オーバーロード | 不可 | 可 |
実務で効いてくるのは「呼び出し」の行です。インスタンスコンストラクターは引数で初期化内容を変えられますが、静的コンストラクターは引数も実行タイミングも外から渡せません。設定値を注入したい初期化を静的コンストラクターに書くと、テスト時に差し替える手段が無くなります。
静的クラス・静的メンバーとの関係
static を付けたフィールドやメソッドは、インスタンスではなく型に属します。静的クラスはインスタンスを作れないため、そのクラスの初期化を担えるのは静的コンストラクターだけになります。また static readonly フィールドは、宣言の一部としてか、静的コンストラクター内でしか代入できません。実行時に計算した値を static readonly に入れたい、という要求が静的コンストラクターを書く典型的な動機です。
静的コンストラクターが実行されるタイミングと初期化の順序
静的コンストラクターにまつわる不具合の大半は、実行タイミングの思い込みから生まれます。ここでは「いつ動きだすのか」「静的フィールド初期化子とどちらが先か」「基底クラスと派生クラスのどちらが先か」を、公式仕様の記述に沿って分けて確認します。
実行の起点は最初のインスタンス生成か静的メンバーの参照
静的コンストラクターは、その型の最初のインスタンスが作成される前、またはそのクラスで宣言された静的メンバーが参照される前に、自動的に呼び出されます。「クラスがメモリにロードされた時点で走る」わけでも「プログラムの開始時に一斉に走る」わけでもありません。触られるまで実行されず、触られた瞬間に実行されます。
ここで見落としやすいのが、契機になるのは「そのクラスで宣言された」静的メンバーであって、基底クラスの静的メンバーではないという点です。派生クラス経由で基底クラスの静的プロパティを読んでも、派生クラスの静的コンストラクターが走るとは限りません。静的コンストラクターの中にログ出力や計測の初期化を置いていると、この差が「初期化されているはずなのに動いていない」という形で表面化します。
静的初期化の4ステップ
| 順序 | 実行される処理 |
|---|---|
| 1 | 静的フィールドがゼロに設定される |
| 2 | 最も派生した型の静的フィールド初期化子 |
| 3 | 基底型のフィールド初期化子(Objectまで) |
| 4 | 階層内のすべての静的コンストラクター |
同じクラス内の静的フィールド初期化子は、クラス宣言に出現するテキストの順序で実行され、その直後に静的コンストラクターの本体が動きます。つまり静的コンストラクターの本体に入った時点で、そのクラスの静的フィールド初期化子はすべて評価済みです。逆に、フィールド初期化子どうしが互いを参照する書き方をすると、宣言を並べ替えただけで値が変わります。
基底クラスと派生クラスで保証されない実行順序
ステップ4について、公式ドキュメントは「静的コンストラクターの実行順序は指定されていません」と明記しています。保証されているのは、階層内のすべての静的コンストラクターがインスタンスの作成前に完了することだけです。基底クラスの静的コンストラクターが派生クラスより先に走ると仮定した設計は、C#では成り立ちません。
この点はJavaと決定的に異なるため、両方を書く現場では特に注意が必要です。Javaはクラス初期化時にスーパークラスを先に初期化すると仕様で定めていますが、C#にその保証はありません。基底と派生の間に初期化の依存関係があるなら、静的コンストラクターの順序に頼らず、明示的な初期化メソッドを一度だけ呼ぶ形に組み替えてください。
シングルトンで実行順序が逆転する公式の例外パターン
「静的コンストラクターはインスタンスコンストラクターより先に実行される」というルールには、公式ドキュメントが明示している例外が1つあります。静的フィールド初期化子がその型のインスタンスを生成する場合、初期化子の評価(インスタンスコンストラクターの呼び出しを含む)が静的コンストラクターより先に行われます。シングルトンパターンで最も起こりやすい形です。
public class Singleton
{
// 静的フィールド初期化子がインスタンスコンストラクターを呼ぶ
private static Singleton instance = new Singleton();
private Singleton()
{
Console.WriteLine("Executes before static constructor.");
}
static Singleton()
{
Console.WriteLine("Executes after instance constructor.");
}
public static Singleton Instance => instance;
}
出力はインスタンスコンストラクター、静的コンストラクターの順になります。ロガーやDIコンテナの初期化を静的コンストラクターに置き、インスタンスの生成を静的フィールド初期化子で行うと、「まだ初期化していないロガーをインスタンスコンストラクターが使う」状態が生まれます。シングルトンでは静的フィールド初期化子と静的コンストラクターを併用せず、どちらか一方に寄せるべきです。生成の責務を型の外に出したい場合は、Factoryパターンによる生成処理の分離が選択肢になります。
静的コンストラクターを書くと外れるbeforefieldinitと最適化への影響
静的コンストラクターには、多くの解説記事が触れていないコストがあります。公式ドキュメントは「静的コンストラクターが存在すると、BeforeFieldInit 型属性を追加できません。これにより、ランタイムの最適化が制限されます」と明記しています。IL上のフラグ名は小文字の beforefieldinit、リフレクションから見えるのが TypeAttributes.BeforeFieldInit で、同じものを指します。このフラグが付いた型は初期化タイミングの制約が緩く、ランタイムが前倒しして初期化する余地があります。静的コンストラクターを1つ書いた時点で、その余地が消えます。
// beforefieldinit が付く。ランタイムは初期化を前倒しでき、最適化が効く
public static class Defaults
{
public static readonly int TimeoutSeconds = 30;
}
// 静的コンストラクターを書いた時点で beforefieldinit は付かなくなる
public static class DefaultsWithCtor
{
public static readonly int TimeoutSeconds;
static DefaultsWithCtor()
{
TimeoutSeconds = 30;
}
}
結論を言い切ります。静的フィールドを定数的な値で埋めるだけなら、静的コンストラクターを書いてはいけません。宣言時に初期化してください。これは筆者の好みではなく、公式ドキュメントが「明示的な静的コンストラクターが必要ない場合は、ランタイムのより適切な最適化のため、静的コンストラクターではなく、宣言時に静的フィールドを初期化します」と推奨している内容です。静的コンストラクターを書く根拠は、複数フィールドにまたがる手続き的な初期化や、例外処理を挟む必要がある場合に限られます。
なお公式ドキュメントは、明示的な静的コンストラクターを置いた場合、初期化例外のトラブルシューティングを助けるためにその存在を文書化するよう求めています。ソースコードに明示していない静的コンストラクターが原因のときは、中間言語(IL)を調べる必要が出てくるためです。型の内部構造を実行時に確認する手段についてはリフレクションで実行時にクラス情報を取得する仕組みが参考になります。
静的コンストラクターの失敗モード:例外とデッドロック
静的コンストラクターが厄介なのは、失敗したときの影響がインスタンスコンストラクターと比べものにならない点です。壊れ方は例外とデッドロックの2種類あり、どちらも復旧手段がプロセスの再起動しかありません。
型が二度と初期化されないTypeInitializationException
静的コンストラクターが例外をスローすると、ランタイムはそれを再度呼び出しません。その型はアプリケーションドメインの有効期間中、初期化されないままになります。以降その型に触れるコードは、多くの場合 TypeInitializationException を受け取ります。しかも TypeInitializationException のメッセージは型名と「型初期化子が例外をスローした」ことしか伝えないため、本当の原因は InnerException を開かないと分かりません。
影響範囲を見誤りやすいのがここです。インスタンスコンストラクターの例外は、その1回の生成が失敗するだけで、次の new は成功しえます。静的コンストラクターの例外は違います。アプリケーションドメインが生きているかぎり、つまり.NET Core以降では実質的にプロセスを再起動するまで、その型が永久に使えません。設定ファイルの読み込みやネットワークアクセスのように失敗しうる処理を静的コンストラクターに置くべきでないのは、この不可逆性が理由です。どうしても静的コンストラクター内で失敗しうる処理を行うなら、本体で例外を捕捉し、型自体は初期化された状態を保ったうえで、フォールバック値か「未設定」を表す状態を保持してください。
本体でブロックしてはいけない操作
静的コンストラクターは、単一のアプリケーションドメイン内で1回だけ、型に基づいてロックされた領域で呼び出されます。したがって本体に追加のロック機構を書く必要はありません。旧来の解説にある「静的コンストラクター内で排他制御を実装する」という記述は不要どころか有害で、二重ロックはデッドロックの起点になります。
そのロックの内側にいるため、公式ドキュメントは静的コンストラクターおよび初期化子の中で現在のスレッドをブロックしないよう明示しています。避けるべき操作は次のとおりです。
- タスク、スレッド、待機ハンドル、イベントの完了を待つこと
- ロックを取得すること
- 並列ループ、
Parallel.Invoke、Parallel LINQ クエリなど、ブロックする並列操作を実行すること
static class TokenCache
{
public static readonly string Token;
static TokenCache()
{
// 静的コンストラクター内でタスクの完了を待つとデッドロックしうる
Token = FetchAsync().GetAwaiter().GetResult();
}
private static Task<string> FetchAsync() => Task.FromResult("token");
}
非同期の初期化を静的コンストラクターに押し込むと、この形になります。C#の静的コンストラクターは非同期にできないため、待ち合わせを消す方法は「静的コンストラクターで初期化しない」しかありません。アプリケーション起動時に一度だけ実行したい初期化は、Generic Hostによる依存関係の管理とDIコンテナに寄せ、ホストの起動処理として明示的に await できる場所へ移してください。ライブラリ側で型の外に初期化を出せない事情があるなら、静的コンストラクターではなく Lazy<T> による遅延初期化に置き換えるのが次善策です。
JavaとC++の静的初期化との違い
「静的コンストラクター」という名前の構文を持つのはC#で、JavaとC++には同じ名前の機能がありません。混同されやすい対応物と、仕様上どこが違うのかを押さえておくと移植時の事故を避けられます。
class Config {
static final String NAME;
// Java の static 初期化子ブロック
static {
NAME = System.getProperty("name");
}
}
Javaの static 初期化子ブロックは役割こそ近いのですが、仕様の保証は別物です。Java言語仕様は、クラスが初期化されるときにそのスーパークラスを先に初期化すると定めており、さらにクラス変数初期化子と static 初期化子を「あたかも単一のブロックであるかのように、テキスト順に」実行すると規定しています。C#が階層内の順序を「指定されていない」としているのと対照的で、Javaの順序前提のコードをそのままC#へ移すと壊れます。
C++には型の初期化をまとめて行う専用の構文がありません。近い挙動を得たいときはブロックスコープの静的変数を使います。C++の規格では、ブロックスコープの静的記憶域変数の動的初期化は「初回に制御がその宣言を通過したとき」に行われ、初期化中に別のスレッドが同じ宣言に到達した場合は初期化の完了を待つと定められています。遅延実行とスレッド安全という点でC#の静的コンストラクターに似ています。ただし規格が完了の待機を義務づけているのは別のスレッドが同時に宣言へ到達した場合であり、初期化中に同じ宣言へ再帰的に制御が戻った場合の動作は未定義と定められています。同じ「スレッドセーフな遅延初期化」でも、自分自身を呼び戻す構造にしたときの保証はC#と同一ではありません。
モジュール初期化子との使い分け
C#にはアセンブリの読み込み時にランタイムが呼び出す ModuleInitializer 属性があり、静的コンストラクターの代わりに使えます。ModuleInitializerAttribute が追加されたのは.NET 5(C# 9)で、APIリファレンスが対応バージョンとして挙げているのも.NET 5以降だけです。.NET Frameworkをターゲットにする場合は同名の属性を自前で定義する必要があります。適用できるのは、静的で、パラメーターがなく、void を返し、そのモジュールから internal または public としてアクセスでき、ジェネリックメソッドでなく、ジェネリッククラスにも含まれず、ローカル関数でもないメソッドだけです。
using System.Runtime.CompilerServices;
internal class Startup
{
// アセンブリの読み込み時にランタイムが呼び出す
[ModuleInitializer]
internal static void InitLogging()
{
}
// 複数付けられるが、InitLogging との実行順序は指定されていない
[ModuleInitializer]
internal static void InitMetrics()
{
}
}
複数のメソッドに付けることもできますが、その場合ランタイムが呼び出す順序は「決定的だが指定されていない」ため、初期化子どうしの順序に依存する設計はできません。用途も限定的で、公式ドキュメントは主な想定をソースコードジェネレーターが生成する初期化コードの置き場所とし、「他のほとんどの場合は、モジュール初期化子ではなく静的コンストラクターを記述する必要があります」と述べています。静的コンストラクターの実行タイミングが遅くて困る、という理由だけでモジュール初期化子へ置き換えるのは筋が悪い判断です。現行のランタイムと言語バージョンで何が使えるかを確認したい場合は、.NET 11とC# 15の新機能とロードマップが参考になります。
よくある質問
静的コンストラクターは1つのクラスに複数書けますか
書けません。クラスまたは構造体が持てる静的コンストラクターは1つだけで、継承もオーバーロードも不可です。継承関係にある複数のクラスがそれぞれ静的コンストラクターを持つことはできますが、C#ではそれらの実行順序は仕様上指定されていません。
静的コンストラクターはプログラムの開始時に呼ばれますか
呼ばれません。実行されるのは、その型の最初のインスタンスが作成される前、またはそのクラスで宣言された静的メンバーが参照される前です。プログラムの起動時点で一斉に走ることはなく、その型が実際に使われるまで実行されません。アセンブリの読み込み時点で実行したい場合は ModuleInitializer 属性(.NET 5・C# 9以降)が該当しますが、用途はソースコードジェネレーターが生成する初期化コードなどに限られ、公式ドキュメントは他のほとんどの場合に静的コンストラクターを推奨しています。
静的フィールド初期化子と静的コンストラクターはどちらが先に実行されますか
同じクラス内では、静的フィールド初期化子がクラス宣言のテキスト順に実行され、その直後に静的コンストラクターの本体が実行されます。ただし静的フィールド初期化子がその型自身のインスタンスを生成している場合は、そのインスタンスコンストラクターの呼び出しが静的コンストラクターより先に行われます。
静的コンストラクターにアクセス修飾子やパラメーターを付けられますか
どちらも付けられません。static と型名、空の丸括弧という形が固定です。呼び出すのはCLRであり、コードから直接呼び出すことも、実行されるタイミングをプログラムから制御することもできません。
C++に静的コンストラクターはありますか
C++に「静的コンストラクター」という構文はありません。遅延初期化とスレッド安全性を得たい場合は、関数の中で宣言したブロックスコープの静的変数を使います。規格では、初回に制御がその宣言を通過したときに初期化され、初期化中に別スレッドが到達した場合は完了を待つと定められています。