3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

C#でTypeLoadExceptionを吐かれて困った出来事

Last updated at Posted at 2023-01-28

それは突然起きた

ある日のこと。

いつものようにC#でプログラムを書いて、いつものように単体テストを実行していると、なんと TypeLoadException を吐かれました。

単体テストの対象になった MyStruct構造体 は以下のような感じになっていました。
(Values<T>構造体MyStruct構造体 に含まれているため、以下のコードに記しておきます)

public readonly struct Values<T>
{
    public required readonly T Zero { get; init; }
    public required readonly T One { get; init; }
}

public readonly struct MyStruct
{
    public readonly int Value = 0;

    public static Values<MyStruct> BasicValues { get; } = new() {
        Zero = new MyStruct(0),
        One = new MyStruct(1)
    };
    
    public MyStruct(int value) {
        Value = value;
    }
}

TypeLoadExceptionとは

Microsoftの公式ドキュメントによると、以下のように説明されています。

TypeLoadException は、共通言語ランタイムがアセンブリ、アセンブリ内の型、または型を読み込むことができない場合にスローされます。

(TypeLoadException クラス (System) | Microsoft Learn)

要するに、「型を管理してる人に『ある型』の情報を求めたけど、その人は知らなかった」ということでしょうか。

どこで例外が起こったか

アセンブリの参照を忘れたかもと思って調べてみましたが、ちゃんと参照されてました。

そうなれば、上のコードの中で TypeLoadException が起きそうな場所は、あそこしかないですね。

そう、ここ。

public struct MuStruct
{
...
    public static Values<MyStruct> BasicValues { get; } = new() {
        Zero = new MyStruct(0),
        One = new MyStruct(1)
    };
...
}

どうして例外が起こったのか

上記の部分を書き換えると、(多分)こんな感じになります。

public struct MyStruct
{
...
    public static Values<MyStruct> BasicValues { get; }

    static MyStruct() {
        BasicValues = new() {
            Zero = new MyStruct(0),
            One = new MyStruct(1)
        };
    }
...
}

ここで、static MyStruct()静的コンストラクタ と呼ばれ、Microsoftの公式ドキュメントによると、以下のように説明されています。

静的コンストラクタにより、最初のインスタンスが作成される前、またはそのクラス (基底クラスではない) で宣言された静的メンバーが参照される前に、クラスが初期化されます。
静的コンストラクターは、インスタンス コンストラクターの前に実行されます。イベントまたはデリゲートに割り当てられている静的メソッドが呼び出されるときは型の静的コンストラクターが呼び出されますが、割り当てられるときは呼び出されません。
静的フィールド変数初期化子が静的コンストラクターのクラスに存在する場合、それらは、クラス宣言に出現するテキストの順序で実行されます。 初期化子は、静的コンストラクターの実行直前に実行されます。

静的コンストラクター - C# プログラミング ガイド | Microsoft Learn
(※一部文言を変更し、一部を太字にしています)

上記の文章の太字部分に注目してください。

これはつまり、「ある型の静的コンストラクタを実行する前に、その型の静的フィールドの初期化を行う」ということです。

よって、上記のコードでは、MyStruct構造体 の静的コンストラクタが実行される前に、MyStruct.BasicValuesフィールド の初期化が行われていることになります。
(MyStruct.BasicValues は実際にはフィールドではないですが、ここでは簡単のため、バッキングフィールドのことを指していると考えてください。)

しかし、この初期化の際に、まだ初期化されていない MyStruct構造体 が使われているために、TypeLoadException が起きたと考えられます。

C#では自己参照的な型の定義ができるので、てっきりこういうのもいい感じにやってくれるのかと思ってました... (しかもこれでコンパイルが通ったので、ここの実装は大丈夫かな?と疑いもしませんでした。)

小話

ちなみに、以下のようなやつは、なぜか例外もなくうまくいきます。
もろちん、ちゃんと参照もできます。

この場合は、静的コンストラクタが呼ばれた直後に、MyStruct.Defaultフィールド の初期化が行われているのでしょうか...??

public readonly struct MyStruct // クラスでも可
{
    public readonly int Value;

    public static MyStruct Default { get; } = new() { Value = 0 }
}

解決策

System.Lazy<T>クラス を活用して、バッキングフィールドを遅延初期化オブジェクトにすればよいです。

例外の原因となっていた部分を削除し、以下のようにします。

public reaodnly struct MyStruct
{
...
    private readonly static Lazy<Values<MyStruct>> _basicValues = new(() => {
        return new Values<MyStruct>() {
            Zero = new MyStruct(0),
            One = new MyStruct(1)
        };
    });

    public static Values<MyStruct> BasicValues { get => _basicValues; }
...
}

System.Lazy<T>クラス は、簡単に遅延初期化が実装できる、とっても便利なクラスです。

初めて Lazy<T>.Valueプロパティ にアクセスされた時に、T型の値の初期化が行われます。

また、スレッドセーフ なのも強いです。
C# (というか.NET) は、自分がほしいものに合うクラスやメソッドが提供されていることがよくあります。手間が省けてありがたい...。

おわりに

実装に関して話すと、「構造体自体が値を提供するのではなく、別のオブジェクトを挟んで値を提供するような実装をしたのも悪いのかも...」と思いました。

しかしながら、これはインターフェースの定義上こうなってしまったので、なんか仕方ない気もします。
(本当はもう少し複雑な仕組みになっているので、インターフェースの定義はどうにも変えられません。)

もしなんか間違ったことを言ってたら、コメントで指摘していただけると嬉しいです。

3
2
2

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
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?