メタいプログラミングをしていると、ある型にある変数を代入可能かどうかを判定したくなることがあります。予想外にはまったんですが、類似の情報があまり見つからなかったので残します。
結論を三行で
- null以外の値の、型変換を伴わない代入可否は簡単に判定できる
- nullの代入可否はちょっと判定が面倒
- 暗黙/明示的な型変換が発生するときはとても面倒
Type.IsInstanceOfTypeを使う
TypeクラスにぴったりなIsInstanceOfTypeメソッドがあります。迷わずこれを使う。
Assert.True(typeof(int).IsInstanceOfType(0));
nullは?
C#8.0以降null非許容参照型が導入されましたが、まだまだ必要悪だと思うnull。代入できるはずですが、Type.IsInstanceOfTypeのリファレンス曰く
false が返されるのは、これらの条件のいずれも満たされない場合、または o が null であるか、現在の Type がオープン ジェネリック型である (つまり、ContainsGenericParameters が true を返す) 場合です。
つまりnullは代入できるのにfalseが返る。IsInstanceOfTypeというメソッド名からもそれは納得です。nullはなんの型のインスタンスでもないですしね。
ですが、同じTypeクラスのIsAssignableFromメソッドも同じ挙動。これはメソッド名から納得できない。だって代入できるじゃん。nullは型情報を失っているから、という仕様は理解できるが納得はできない。
翻って、nullを代入できるのってなんだっけ、と考えると、参照型じゃん!となるのですが、ダウト。Nullable<>は構造体で値型、でもnullを代入できる。
ここら辺で理解不足が露呈したのでドキュメントを読み直します。
null キーワードは、いかなるオブジェクトも参照していない null 参照を表すリテラルです。 null は参照型変数の既定値です。 null 許容値型を除き、通常の値の型を null にすることはできません。
(null : https://docs.microsoft.com/ja-jp/dotnet/csharp/language-reference/keywords/null
ということは、
- 参照型 → nullが既定値、つまりnullを代入できる
- 値型 → Nullable<>だけnullを代入できる。他は不可
でよさそう。ということで実装。
!type.IsValueType || (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>));
がTrueならnullを代入可能
型変換は?
型変換してしれっと代入できる組み合わせもありますよね。これはType.IsInstanceOfTypeもType.IsAssignableFromも対応してくれない。
Assert.False(typeof(long).IsAssignableFrom(typeof(int)));
Assert.False(typeof(int).IsAssignableFrom(typeof(long)));
そもそも型変換にもいくつか種類がある。
キャストと型変換を読み込みます。
暗黙的な型変換
これはさらに2つに分かれます。
暗黙的な数値変換
intをlongに代入できるような、組み込み数値型で起きる暗黙の型変換。一発で判定できる便利なメソッドは見つかりませんでした。これは「組み込みの数値変換」で対応表ができているので、判定したかったら頑張ってコードに起こす。おそらくこれを判定したくなる場合は発生しないので今回は割愛。
参照型の型変換
参照型の場合は、直接または間接的な基底クラスやインターフェイスに暗黙的に型変換されます。これはIsAssignableFromで判定可能。
明示的な型変換
これも組み込み数値型の変換(例:intをdoubleに)と参照型の変換(例:StreamをMemoryStreamに)にわかれます。数値型についてはこれも対応表から頑張ってコードを書く。参照型についてはIsAssignableFromで判定可能。
ユーザー定義の型変換
ユーザー定義の変換演算子が実装されているかどうか、これを一発で判定するメソッドも見つかりませんでした。リフレクション使って頑張って判定できるんじゃないでしょうか。おそらくこれを判定したくなる場合は発生しないので今回は割愛。
ヘルパークラスを使用する変換
型変換の種類として載っていたのでここにも記載しましたが、そこまで考えて判定できるはずもないので割愛。
最終的に
今回やりたかった目的の範囲内では下記メソッドに落ち着きました。
static class TypeEx
{
public static bool IsAssignableFromEx(this Type type, object obj)
{
if (obj != null)
{
return type.IsInstanceOfType(obj);
}
else
{
return !type.IsValueType ||
(type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>));
}
}
}
これで判定できる代入可否は以下の通りになります。
- 非nullのインスタンスが代入可能かどうか
- ただし、数値型の暗黙の型変換が起きる場合は除く
- nullが代入可能かどうか
おわりに
日頃なにげなく書きまくっている代入がこんなに面倒くさいとは。コンパイラ様様です。この際コンパイラに全部お任せして、式木とかCodeDOMを使えばもっと正確に判定できるのでは…と思いつつ、絶対遅いのでやらないと思います。