「コンパイルは通るのに、なぜか実行時だけ例外が出る」
System.Text.Json 周りで、そんな現象に遭遇しました。
- 型は見える
- メソッドも存在する
- でも実行すると落ちる
原因を追っていくと、
C# の書き方ではなく、.NET(CLR)の挙動に行き着いた、という話です。
起きていたこと(要約)
- NuGet 経由で System.Text.Json を参照している DLL が複数存在
- それぞれが 異なるバージョンの System.Text.Json を前提にビルドされていた
- 使用している API はどのバージョンにも存在していたため コンパイルは問題なし
- 実行時にロードされた実体が合わず、例外が発生
なぜコンパイルは通ったのか
コンパイル時に参照しているのは 参照アセンブリ です。
参照アセンブリに含まれるのは、
- 型名
- メソッド名
- 引数・戻り値
といった シグネチャ情報のみ です。
public static string Serialize<T>(T value);
この定義が複数バージョンに存在していれば、
コンパイラはそれらの違いを認識できません。
「そのメソッドが存在する」ことが分かれば OK
という世界です。
実行時に何が起きるか
実行時に必要になるのは 実行用アセンブリ(実装 DLL) です。
ここで初めて、
- IL の中身
- 内部実装
- バージョン依存の処理
が要求されます。
その結果、
- MissingMethodException
- TypeLoadException
- FileLoadException
といった例外が発生しました。
ファイルサイズで気づけることもある
後から振り返ると、
ロードされていた DLL の ファイルサイズが異常に小さい ことに気づきました。
- 実行用 DLL:数百 KB 〜 数 MB
- 参照用 DLL:数十 KB 程度
「コードが無いように見える」
という感覚は、実は正しかったようです。
学んだこと
- 参照が通る ≠ 実行できる
- 参照アセンブリは「契約」しか保証しない
- 実体をロードするのは CLR
- 問題はコードではなく、依存関係と解決経路 にあることが多い
再発防止として意識していること
- BCL(System.*)を NuGet でむやみに追加しない
- 上位 DLL の TargetFramework を必ず確認する
- 実行フォルダの DLL の サイズと配置場所を見る
- .deps.json / runtimeconfig.json を疑う
おわりに
今回の件で、
「C# を書いている」
から
「.NET がどう動くかを意識する」
という視点に一段階進んだ気がしています。
同じように、
- 参照は通るのに実行時だけ落ちる
- 環境差分で原因不明の例外が出る
といった経験をした人の、整理材料になれば幸いです。
参照が通ることと、実行できることは別物