僕はついさっき知ったばかりなんですけど、C#の数値0ってあらゆるenum値に暗黙的キャストされるんですってね。要するに、0だけはキャスト不要で列挙型の値として変数に代入できちゃったりするということ。コードで示すと以下のようなかんじ。
using System; public class Program { enum Hoge { A, B } enum Piyo { C, D } public static void Main() { Hoge h1 = 0, h2 = 0.0f; Piyo p1 = 0x0, p2 = 0.0m; Console.WriteLine($"{h1}, {h2}"); Console.WriteLine($"{p1}, {p2}"); } }
このコードは何の警告もなくビルドが通り、以下の実行結果が得られる。
A, A C, C
浮動小数点数やDecimalのゼロも同様の扱いっていうのが、なかなかのキモいポイント。C#のenumってC/C++のそれと違ってそれなりに厳密なので、自分的には結構衝撃的な仕様だった。まぁ、分かってしまえばどうってことはない。
で、ここからが本題。
そんな0のenumへの暗黙的型変換のおかげで、意図せぬメソッドのオーバーロード解決が行われ、小一時間ハマった。
以下のような処理メソッドAdd
と、それに対する単体テストAddTest
を考える。対象メソッドはプライベートな静的メソッドなので、テスト呼び出しにはPrivateType.InvokeStatic
メソッドを使っているが、至ってシンプルなコードである。
何の疑いもなく動くと思いきや、ケース3のテストでのみMissingMethodException
例外が発生し、Add
メソッドの呼び出しに失敗するのだ!
// 加算 private static decimal? Add(decimal? v1, decimal? v2) { return v1 + v2; } // Addメソッドの単体テスト void AddTest() { var privateType = new PrivateType(typeof(AddClass)); // ケース1 var value = (decimal?)privateType.InvokeStatic("Add", 1.0m, 1.0m); // OK Assert.AreEqual(2, value); // ケース2 value = (decimal?)privateType.InvokeStatic("Add", 1.0m, 0.0m); // OK Assert.AreEqual(1, value); // ケース3 value = (decimal?)privateType.InvokeStatic("Add", 0.0m, 1.0m); // MissingMethodException例外が発生! Assert.AreEqual(1, value); }
MissingMethodException
は読んで字のごとく、メソッドが見つからなかった場合に投げられる例外だ。
ケース1~2は通っているのに見つからないとは一体…!?という感じなのだが、これまでの説明からお気づきであろう、ケース1~2と3では呼び出されるInvokeStaticのシグネチャが違うのだ。同メソッドは引数違いで10個ほどのオーバーロードが定義されており、それぞれ以下のシグネチャのものが呼び出される。
ケース3では、第二引数の0.0mがBindingFlagsに暗黙キャストされた結果、引数を1つ持つAdd
メソッドを呼び出そうとして例外を吐くというわけ。なんじゃそりゃー!分かるわけネェーッ!!意図通り動かすにはprivateType.InvokeStatic(“Add”, (decimal?)0.0m, 1.0m)
のように、明示的にキャストし正しいオーバーロード解決を導いてあげればよい。
この暗黙型変換を禁止ないし警告出してくれるコンパイルオプションとかないのかしら…ハマった時に死ぬほどわかりづらいんですけど……