Ver. 4.0
dynamic って内部的にはどうなってるの?という話。
C# の dynamic は、「型が動的」というよりは、「静的な型に対する動的コード生成」と言った方が正確です。 動的に生成したコードはキャッシュされていて、2度目の呼び出しからはかなり効率よく実行されます。 このような手法はインラインメソッドキャッシュ(inline method cache)と呼ばれています。
注意: 内部的な話なので、C# のバージョンアップで実装方法が変わる可能性もあります(基本的な原理は変わらないと思いますが)。 (今このページに書かれている内容は、C# 4.0 の時点の実装に基づいています。)
dynamic を使ったコードは、内部的には CallSite というクラスを使ったコードに展開されます。 (多分、「動的呼び出し(call)用の動的コードを生成するための用地(site)というような意味合い。) 例えば、以下のような C# 4.0 コードは、
public static void CallX(object obj) { dynamic d = obj; d.X(); } public static dynamic GetX(dynamic obj) { return obj.X; }
以下のようなコードに展開されます。
// ↓本当は、いかにもコンパイラが自動生成したような変な変数名になってる static CallSite<Action<CallSite, object> site1; static CallSite<Func<CallSite, object, object> site2; public static void CallX(object obj) { object d = obj; // (1) dynamic 型の変数は、実のところ単なる object 型になる if (site1 == null) { // d.X() 相当のコードを動的生成するための CallSite を作る。 site1 = CallSite<Action<CallSite, object>>.Create( new CSharpInvokeMemberBinder( CSharpCallFlags.None, "X", typeof(DynamicSample), null, new CSharpArgumentInfo[] { new CSharpArgumentInfo(CSharpArgumentInfoFlags.None, null) })); } // 動的生成したコードを呼んだり、新たに動的生成するのは、 // 実際には Target デリゲートの中。 site1.Target.Invoke(site1, d); } // 引数や戻り値が dynamic の場合は、Dynamic 属性付きの object 型になる [return: Dynamic] public static object GetX([Dynamic] object obj) { if (site2 == null) { // d.X 相当のコードを動的生成するための CallSite を作る。 site2 = CallSite<Func<CallSite, object, object>>.Create( new CSharpGetMemberBinder( "X", typeof(DynamicSample), new CSharpArgumentInfo[] { new CSharpArgumentInfo(CSharpArgumentInfoFlags.None, null) })); } // 動的生成したコードを呼んだり、新たに動的生成するのは、 // 実際には Target デリゲートの中。 return site2.Target.Invoke(site2, obj); }
要点は3つ。
前述の通り、コンパイル結果的には dynamic 型ってものはなくて、 実際のところは単なる object 型の変数になります。 特に、ローカル変数の型を dynamic にした場合には、完璧に単なる object 型の変数になります。
メンバー変数やプロパティ、メソッドの引数や戻り値の型を dynamic にした場合には、 普通の object と区別するために、Dynamic 属性が付きます。
dynamic x; public dynamic X { get { return x; } set { x = value; } } public static dynamic GetX(dynamic obj) { // 中身省略 }
というコードは、以下のようなコードに変換されます。
[Dynamic] private object x; [Dynamic] public object X { [return: Dynamic] get { return this.x; } [param: Dynamic] set { this.x = value; } } [return: Dynamic] public static object GetX([Dynamic] object obj) { // 中身省略 }
なので、以下のようなコードはコンパイルエラーを起こしたりします。 (dynamic 型と object 型でメソッドをオーバーロードすることはできません。)
// 同じパラメーター型の GetX が2個あるぞって怒られる。 public static dynamic GetX(dynamic obj) { return obj.X; } public static object GetX(object obj) { var t = obj.GetType(); return t.GetMethod("X").Invoke(obj, new object[0]); }
ジェネリック型の型引数を dynamic にした場合はどうなるかというと、
static void GenericDynamic( IDictionary<object, object> a, IDictionary<dynamic, object> b, IDictionary<object, dynamic> c, IDictionary<dynamic, dynamic> d) { }
この例の場合、以下のようなコードに変換されます。
private static void GenericDynamic( IDictionary<object, object> a, [Dynamic(new bool[] { false, true, false })] IDictionary<object, object> b, [Dynamic(new bool[] { false, false, true })] IDictionary<object, object> c, [Dynamic(new bool[] { false, true, true })] IDictionary<object, object> d) { }
要するに、型引数の少なくともどれか1つが dynamic 型だった場合、 bool[] の引数付きの Dynamic 属性が付きます。
続いて CallSite の初期化部分。 上述のコードのうち、以下のようなコードの部分について。
if (site2 == null) { // d.X 相当のコードを動的生成するための CallSite を作る。 site2 = CallSite<Func<CallSite, object, object>>.Create( new CSharpGetMemberBinder( "X", typeof(DynamicSample), new CSharpArgumentInfo[] { new CSharpArgumentInfo(CSharpArgumentInfoFlags.None, null) })); }
d.X などのメンバーアクセスに対して、どういう動的コード生成を行えばいいかは、CallSite クラス自身は知りません。
それを実際に担ってるのは、この例で言うと CSharpGetMemberBinder の部分です。
CSharpGetMemberBinder は System.Runtime.CompilerServices.CallSiteBinder というクラスを継承していて、
CallSiteBinder の抽象メソッドの Bind 内で式木を作っています。
CSharpGetMemberBinder というように、名前に CSharp という言葉が付いてることからもわかるように、 言語ごとに CallSiteBinder の実装を変えることができます。
(CallSite 自体は、C# 4.0 の dynamic のためだけに作られたクラスではなくて、 DLR に含まれているクラス。 IronPython などの動的言語の実装にも使われています。)
C# 4.0 の場合(要するに、CSharpGetMemberBinder の中の挙動としては)、以下のような動的コード生成を行います。
最後に、実際に動的コード生成。 CallSite.Target デリゲートを呼んでいる部分について。
site1.Target.Invoke(site1, d);
Target デリゲートの中身は、初期状態では以下のようなコードと同じ状態になっています。
static object _anonymous(CallSite site, object x) { return site.Update(site, x); }
この状態で、GetX(new Point { X = 1, Y = 2}); というように、
Point 型のインスタンスを引数として Target が呼ばれたとします。
このとき、Target 内には Update の1行しかないので、
この Update が呼ばれて、動的コード生成が行われます。
その結果、Target が以下のような状態に更新されます。
static object _anonymous(CallSite site, object x) { if (x is Point) return ((Point)x).X; else return site.Update(site, x); }
ここで、((Point)x).X の部分を生成するのが CallSiteBinder の役目です。
Target 内がこのような状態になったので、 以後、Point 型のインスタンスで GetX を呼べば、 そこそこいい実行速度が得られます。
実行速度に関してもう少し詳しく言うと、
x.X プロパティを呼ぶのと比べると、if 文とキャストの分だけ遅い。
という感じになります。
( ちなみに、こういう、動的コード生成してデリゲート化しておくような手法をインラインメソッドキャッシュ(inline method cache)と言うようです。 DLR や C# 4.0 以外(例えば JavaScript とか)でも、同様の手法はよく使われます。 )
Point 以外の型のインスタンスが来ると、当然また Target の更新がかかります。
例えば、Vector3D を使って GetX(new Vector3D(1, 2, 3)); とかすると、
static object _anonymous(CallSite site, object x) { if (x is Point) return ((Point)x).X; else if (x is Vector3D) return ((Vector3D)x).X; else return site.Update(site, x); }
となります。
その他、いくつか小ネタを。
typeof(dynamic) はそもそもエラーになります。
typeof(object) が得られたりはしません。
dynamic の実体は object なわけですが、 じゃあ、ToString() や GetHashCode() 等の object 型のメソッドはどうなるかというと・・・、 CallSite を介した動的コード生成になります。 ToString() だけ特別扱いされて静的なコードになったりはしません。
dynamic キーワードを使わず、 Dynamic 属性を直接付けようとするとコンパイルエラーになります。 「DynamicAttribute は直接は使えない。dynamic キーワードを使ってくれ」というような感じで怒られます。