DynamicObjectでより沢山の型情報を取る方法
- 2011-07-25
Chaining AssertionのAsDynamicに少し不具合があったので、ver.1.5.0.0として更新しました。今まではメソッドの引数にnullを渡すと死んでました。ぬるぽ!まあしかし、引数そのもののからしかTypeが取れなかったので、nullだと、どのみちメソッドを特定するための型情報がないからオーバーロードの解決は不可能なので、仕様ですよ仕様、という言い訳。
などとふざけたことを思っていたのですけれど、コンパイル時に決定される引数の型を取り出す方法が判明したので、そのへんも含めて完全に解決しました。やったね。その方法は、というと
public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result)
{
var csharpBinder = binder.GetType().GetInterface("Microsoft.CSharp.RuntimeBinder.ICSharpInvokeOrInvokeMemberBinder");
if (csharpBinder == null) throw new ArgumentException("is not csharp code"); // CSharpコードではない
// ジェネリックの型引数の取得(Hoge<T>(1, t)とかでのTのこと)
var typeArgs = (csharpBinder.GetProperty("TypeArguments").GetValue(binder, null) as IList<Type>).ToArray();
// コンパイル時に決定されているパラメータの型の取得
var parameterTypes = (binder.GetType().GetField("Cache", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(binder) as Dictionary<Type, object>)
.First()
.Key
.GetGenericArguments()
.Skip(2)
.Take(args.Length)
.ToArray();
// それらの情報からMethodInfoを特定する
var method = MatchMethod(binder.Name, args, typeArgs, parameterTypes);
// 呼び出し
result = method.Invoke(target, args);
return true; // 呼べてれば必ずTrueなので。
}
……。ひどそうな匂いが!まず、素のままでは情報が足りなすぎるので、基本的にリフレクション全開です。その中でも、parameterTypesが今回追加したところです。binderのCacheに、CallSiteのデリゲート(dynamicを使って呼び出すと、コンパイル時にこの辺のものが自動生成される)があるので、そこから型情報を持ってこれることに気づいたのだ(キリッ
もう少し詳しく説明しますと
// このヘンテツもないどうでもいいコードは
dynamic d = null;
var result = d.Hoge<string, int>(100, (string)null, (ICollection<int>)null);
// コンパイル後はこんな結果に化けちゃいますあら不思議!
object d = null;
if (Program.<Main>o__SiteContainer0.<>p__Site1 == null)
{
Program.<Main>o__SiteContainer0.<>p__Site1 = CallSite<Func<CallSite, object, int, string, ICollection<int>, object>>.Create(Binder.InvokeMember(CSharpBinderFlags.None, "Hoge", new Type[]
{
typeof(string),
typeof(int)
}, typeof(Program), new CSharpArgumentInfo[]
{
CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null),
CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.UseCompileTimeType | CSharpArgumentInfoFlags.Constant, null),
CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.UseCompileTimeType | CSharpArgumentInfoFlags.Constant, null),
CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.UseCompileTimeType | CSharpArgumentInfoFlags.Constant, null)
}));
}
object result = Program.<Main>o__SiteContainer0.<>p__Site1.Target(Program.<Main>o__SiteContainer0.<>p__Site1, d, 100, null, null);
細かい部分はどうでもいいので、良く分からないFuncが生成されてるんだな、というとこだけ見てもらえれば。さて、これを頭に入れた上で、DynamicObjectのTryInvokeMemberでbinderの奥底のCacheを探してみると、
このFunc6というものが、Func<CallSite, object, int, string, ICollection<int>, object>です、発見出来ました!これの型引数が、コード上での呼び出し時の型引数になるわけです。なお、第一引数はCallSite、第二引数はインスタンスなので無視してSkip(2)、そして引数の個数分だけTake(まあ、ようするに最後が戻り値の型なわけですが)。
というわけで、実際に改善されたAsDynamicを使ってみますと、
// こんな何のヘンテツもないオーバーロードのあるクラスがあるとして
public class PrivateClass
{
private string Hoge(IEnumerable<int> xs)
{
return "enumerable";
}
private string Hoge(List<int> xs)
{
return "list";
}
}
// 型はdynamicです←意地でもvarで書きたい人
var mock = new PrivateClass().AsDynamic();
// 型でオーバーロード分けが出来るようになった!
List<int> list = new List<int>();
IEnumerable<int> enumerable = new List<int>();
(mock.Hoge(list) as string).Is("list");
(mock.Hoge(enumerable) as string).Is("enumerable");
というわけで、より正確なオーバーロードの解決が図れるようになりました。何をアタリマエのことを言ってるんだお前は、と思うかもしれませんが、TryInvokeMemberに渡ってくる情報はobject[] argsなのです。args[0]の型はobjectなわけで、それをGetTypeしたら、出てくるのはList<int>なのです。何をどうやっても、.csファイルではIEnumerable<int>と書かれているという情報を得ることは出来なかったわけです、今までは。ましてやnullだったら型もヘッタクレもなかったわけです。でもこれからは違う。コード上のデータが取れる!
などとツラツラと書いてみましたが、利用者的にはどうでもいい話ですね、はい。それに、リフレクションはいいとしても、Cacheって何よ?CacheのFirstが決め打ちなのって何よ?などなどは、ぶっちゃけよくわかっていなくて(だって全部internalだしね……)若干怖いのですが、まあ、多分、大丈夫でしょう、多分……。それと、まだ完璧じゃあなくてサポートしてないオーバーロードのケースが幾つかあります。とはいえ、ほとんどのシチュエーションでは問題ないのではかと思います。
まとめ
このChaining Assertionですが、私は結構普通に使いまくっていて、ないと死にます。激しく便利。そうそう、紹介しますと、 actual.Is(expected) と、メソッドチェーン形式で流れるようにアサーションが書けます。ラムダ式での指定やコレクションへの可変長引数など、ちょうどかゆいところに手が届く拡張を施してあって、随分とテストを書くのが楽になります。
AsDynamicは、オマケ機能というか。privateなメソッドやプロパティ、フィールドにもdynamicを通してアクセス出来るように変換します。たまにしか使いませんが(MSTestにはPrivate Accessorがあるので)、あると便利よね、という時もそこそこあり。
MSTestだけではなく、NUnitやMbUnit、xUnit.NETでも使えますので&NuGet経由でも入れられますので、一度是非お試しを。