MethodInfo.GetGenericArguments() と System.RuntimeType と配列の covariance
概要
配列の covariance のおかげでデバッグに手間が掛かった、という話。
キーワードについて
- MethodInfo.GetGenericArguments メソッド
- これは、メソッドの型引数を表す Type[ ] を返す。
- System.RuntimeType は、System.Type の .net 上における実装。
- System.Type は public abstract class
- System.RuntimeType は internal class
- この RuntimeType は、mono 環境に存在しない。
- 配列の covariance とは、例えば A is B ならば A[ ] を B[ ] に暗黙に変換できるということ
ひどい目
SUtils(https://www.assembla.com/wiki/show/SUtils)の持っているリモートオブジェクト機構(SUtils.Remoting)にて、Generic なメソッドをサポートしていたのだが、これがなぜか mono 上のサーバーと .net 上のクライアントでのみ正常に動作しないというバグが出たのが発端。
このバグでは、mono 環境上のサーバープロセスにて、デシリアライザにて System.RuntimeType が発見できなかった旨の例外が発生していた。
しかしここで不思議なのは、SUtils.Remoting では、RuntimeType を通信に使っている型の定義や、そこから使われている型には含まれないように意図的に排除していたのである。にも関わらず RuntimeType の型情報がシリアライズされ、mono 環境にて検出されてしまったのである。
じつはこれは、MethodInfo.GetGenericArguments() が、Type[ ] ではなく RuntimeType[ ] を返していたのが原因であった。
イメージ図
class HogeMessage{ // Generic なメソッドの型引数を覚えておく // Type や Type[ ] は安心してシリアライズ出来ると分かっている public Type[ ] TypeArguments; } HogeMessage msg = ...; // GetGenericArguments() の戻り型は Type[ ]。 // でも、実際には RuntimeType[ ] インスタンスが戻る。 msg.TypeArguments = method.GetGenericArguments();
すなわち、GetGenericArguments() が返す Type[ ] (上の例で HogeMessage.TypeArguments)をそのままシリアライズした所、それは実際には RuntimeType[ ] であった故に、mono 環境でのデシリアライズで RuntimeType 何それ美味しいの、な状態になってコケてしまったのである。
問題の一般化
今回の事例は、.net と mono 環境の間でシリアライズされたデータを相互にやりとりしていて、かつ片方にしかない型がある、という一般的にはあまりないであろうシチュエーションであった。
しかしながらも、例えばメソッドなどが、シグニチャ上の型とは違う型の配列を返しうるということは、例えば以下のような問題も起こりうることを意味している。
class A{} class B : A{} A[ ] array = (A[ ] を返すメソッドの戻り値); array[0] = new A(); // ArrayTypeMissmatchException
このように、配列の covariance が通る C#/CLI など*1において、GetGenericArguments() のように、シグニチャ上の型(Type[ ])とは違う型の配列(RuntimeType[ ])を戻す実装をすると、使う側にとって予期せぬバグを招くため、危険である。
なので、配列型を返すメンバなどを宣言した場合は、返すインスタンスはその宣言と同じ型になっているべきであろう。
また、GetGenericArguments() のように、正体不明な配列インスタンスを返すメソッドを呼ぶ場合、戻り値の配列はいったん列挙して、必要なら別のコンテナに移すようにした方が、より安全であろう。
(というか、個人的には配列の covariance なんざ使うべきではないとも思っている。そうであるが故に、今回の GetGenericArguments の件は想定外だったのだが・・・。)