概要
Ver. 9
プログラムの実行時、最初に1回だけ呼び出したい処理が必要になることがあります。 「静的コンストラクター」で説明しているように、この静的コンストラクターという機能を使っても「最初に1回だけ呼ばれる」ということができますが、C# 9.0 ではモジュール初期化子という書き方もできるようになりました。
モジュール初期化子
「静的コンストラクター」を使うとプログラム中で1回だけ呼び出される処理を書くことができます。 静的コンストラクターが呼び出されるタイミングは、そのクラスのなんらかのメンバーに初めてアクセスしたときです。
C# 9.0 では、もう1種類、「最初に1回だけ呼ばれる」という性質の処理の書き方ができるようになりました。
以下のように、ModuleInitilizer
属性(System.Runtime.CompilerServices
名前空間)を付けた静的メソッドを書くと、それが必ず1回呼び出されるようになります。
using System;
using System.Runtime.CompilerServices;
class Sample
{
[ModuleInitializer]
public static void Init()
{
Console.WriteLine("必ず1回だけ呼ばれる");
}
}
これをモジュール初期化子(module initializer)と呼びます。
静的コンストラクターとの差は以下の通りです。
-
1つのクラスに複数のモジュール初期化子を書ける
partial
を使って複数のファイルに分かれていても全部呼ばれる
- そのクラスを含んでいるモジュールを読み込んだ時点で呼ばれる
静的コンストラクターの呼び出しには「そのクラスのなんらかのメンバーにアクセス」という条件が付くので、確実に呼び出される保証が実はなかったりします。 モジュール初期化子の呼び出しも「モジュールを読み込む」(モジュールに含まれているなんらかの型に触れる)という条件は付くんですが、静的コンストラクターと比べればだいぶ確実に呼ばれます。 (一切何の型も使わないモジュールを参照すること自体がほとんどないので、実質的には「確実」と行ってしまっても構わないと思います。)
モジュール初期化子の実装方法
「モジュール読み込み時に必ず呼ばれる」というもの自体は .NET Framework 1.0 の頃から実はありました(単に C# から使う手段がなかっただけ)。
当初から、「<Module>
という特殊な名前のクラスの静的コンストラクターは、モジュール読み込み時に必ず1回呼ばれる」という仕様があります。
<>
を含む名前なので通常の C# コードで書くことはできませんし、C# 8.0 まではこの型の静的コンストラクターを書き出す手段もありませんでした。
C# 9.0 のモジュール初期化子がやっていることはこの「<Module>
クラスの静的コンストラクターの生成」です。
例えば以下のようなコードを書いたとすると、
using System.Runtime.CompilerServices;
class C1
{
[ModuleInitializer]
public static void Init1() { }
[ModuleInitializer]
public static void Init2() { }
}
class C2
{
[ModuleInitializer]
public static void Init1() { }
}
以下のようなコードに相当するものがコンパイラーによって追加されます。
class <Module>
{
static <Module>()
{
C1.Init1();
C1.Init2();
C2.Init1();
}
}
1つの静的コンストラクターの中に単なるメソッド呼び出しが並べられているだけの状態になります。 したがって、以下のような性質があります。
-
トータルでの呼び出しコストは静的コンストラクターをたくさん並べるよりも軽い
- (静的コンストラクターは「マルチスレッド実行時でも1回限り呼ぶ」という処理が必要で、通常のメソッド呼び出しよりも少し負担が大きい。モジュール初期化子はその負担が1回だけで済む)
- (意図せずコードを残してしまうと)本当は不要であっても必ず呼ばれる
- 呼び出しの負荷がモジュール読み込み時に集中する
モジュール初期化子の用途
.NET 6.0 では iOS や WebAssembly 上での実行のサポートが入ります。 (iOS や WebAssembly 上で C# が動くという状況はもっと前からあったんですが、 Windows で動いていた .NET とは別系統で保守されていました。 それが、 .NET 6.0 で統合されて1つの「.NET」になりました。)
用途が増えると、これまでの用途では動いていたものが新しい用途では動かせない・動いても効率が悪いということがあります。 実際、iOS や WebAssembly 環境ではリフレクションを使いづらいです。
例えば以下のように、文字列で型名を指定して、その型のインスタンスを生成するということを考えてみます。 (こういうコードをそのまま書くことはないですが、JSON などにシリアライズ・デシリアライズしたりするときにこれに類する処理が内部的に行われたりします。)
using System;
using System.Reflection;
// リフレクションを使えば文字列からその名前の型のインスタンスを作れる。
// ただ、パフォーマンスはあんまりよくない。
object? CreateInstance(string typeName)
{
if (Assembly.GetExecutingAssembly().GetType(typeName) is { } t) return Activator.CreateInstance(t);
else return null;
}
// ただ、 "A", "B" という文字列が型名を指しているかどうかはコンパイラーが関知することではなく、
// 「クラス A, B は誰も使っていない」誤判定を受けることがある。
// AOT (事前ネイティブコード化)実行環境だと A, B が消し去られて、上記 GetType に失敗しうる。
Console.WriteLine(CreateInstance("A"));
Console.WriteLine(CreateInstance("B"));
class A
{
}
class B
{
}
このコードは直接的にクラス A
、B
を使っているコードがどこにもありません。
かなり頑張ってコードを追えば、"A"
という文字列が A
というクラスを指していて、
それをリフレクション(Activator.CreateInstance
)越しに使っていることがわからなくはないんですが、コンパイラーが機械的に判定すると「A
も B
も使われていない」という判定を受けます。
一方で、C# 9.0 の世代では source generator (ソースコード生成)の仕組みが導入されました。
source generator 導入の動機の1つに「これまでリフレクションでやっていたような処理をコンパイル時にやりたい」というものがあります。
先ほどの Activator.CreateInstance
を使っていた処理も、source generator を使って、「最初に1回どこかで初期化処理をする」みたいなものに置き換えることが考えられます。
例えば、以下のように、CreateInstance
的な処理を自前管理することを考えます。
using System;
using System.Collections.Generic;
// どこか必ず1回呼ばれる保証のあるものを使って、事前に string → Func<object> な辞書を作っておくという発想。
static class TypeRepository
{
private static readonly Dictionary<string, Func<object>> _factories = new();
// 型名からインスタンスを作る。Register がどこかで呼ばれる前提。
public static object? CreateInstance(string typeName) => _factories.TryGetValue(typeName, out var f) ? f() : null;
// 型名 → インスタンス生成デリゲートを登録。
// 静的コンストラクターで呼んでもらう想定だと破綻気味だったけど、モジュール初期化子なら割と成立する。
public static void Register(string typeName, Func<object> factory) => _factories.Add(typeName, factory);
}
ここで静的コンストラクターだと「呼ばれる保証がない」という点が問題になります。 例えば以下のコードのように変な挙動をしたりします。
using System;
using System.Collections.Generic;
// 後述するように、静的コンストラクターはこの用途だと呼ばれない。
// なので、Register が呼ばれてなくて、CreateInstance が null を返す。
Console.WriteLine(TypeRepository.CreateInstance("A")); // null
Console.WriteLine(TypeRepository.CreateInstance("B")); // null
// これが例えば、どこでもいいから1度 A のメンバーを空呼びすると上記コードがちゃんと new A(), new B() を返すようになる。
// 静的コンストラクターが呼ばれるタイミングは「その型のメンバーを最初に使った直後」
_ = new A(); // このタイミングで A の静的コンストラクターが呼ばれる
_ = new B(); // このタイミングで B の静的コンストラクターが呼ばれる
Console.WriteLine(TypeRepository.CreateInstance("A")); // A
Console.WriteLine(TypeRepository.CreateInstance("B")); // B
// 手書きはあまりしたくないものの、Source Generator がある今、
// 必要な型に対して以下のようなコード生成をするのは十分現実的。
// ただ、静的コンストラクターは呼ばれるタイミングに問題があって…
class A
{
static A() => TypeRepository.Register(nameof(A), () => new A());
}
class B
{
static B() => TypeRepository.Register(nameof(B), () => new B());
}
モジュール初期化子なら確実に呼ばれる保証が強いのでこの問題を解決できます。 以下のコードであれば意図した挙動になります。
using System;
using System.Runtime.CompilerServices;
// モジュール初期化子の場合、その型を含むモジュール(dll とか exe とか)がロードされた直後に必ず呼ばれる。
// 静的コンストラクターの「型に触れた瞬間」よりは確実に呼ばれる保証あり。
Console.WriteLine(TypeRepository.CreateInstance("A")); // A
Console.WriteLine(TypeRepository.CreateInstance("B")); // B
// 静的コンストラクターだと呼ばれるタイミングが不定で問題があったけど、モジュール初期化子なら大丈夫。
class A
{
[ModuleInitializer]
public static void Init() => TypeRepository.Register(nameof(A), () => new A());
}
class B
{
[ModuleInitializer]
public static void Init() => TypeRepository.Register(nameof(B), () => new B());
}
ジェネリックな型
逆に静的コンストラクターでないと書けないものもあります。 ジェネリックな型に対してはモジュール初期化子を定義できません。
public class Generic<T>
{
// これはコンパイル エラー。
// 静的コンストラクターなら、 Generic<int> みたいな具象化した型ごとに呼ばれるけど、
// モジュール初期化のタイミングでは何の型で具象化されるかわからなくて呼びようがない。
[ModuleInitializer]
public static void Init1() { }
}
前節で書いたような用途でモジュール初期化をジェネリック型に対して使いたい場合、 以下のように、非ジェネリックな型を1つ用意して、その中で想定しうるすべての型を列挙するなどの対処が必要になります。
// 前節のようなことをジェネリックな型に対してしようとすると…
class Generic<T>
{
}
// 非ジェネリックなものを1個用意して、
class Generic
{
[ModuleInitializer]
public static void Init()
{
TypeRepository.Register(typeof(Generic<>) + "<int>", () => new Generic<int>());
TypeRepository.Register(typeof(Generic<>) + "<string>", () => new Generic<string>());
// 以下、使うことがわかっている限りの具象型を並べる必要がある。
}
}