概要
Ver. 4.0
dynamic って内部的にはどうなってるの?という話。
C# の dynamic は、「型が動的」というよりは、「静的な型に対する動的コード生成」と言った方が正確です。 動的に生成したコードはキャッシュされていて、2度目の呼び出しからはかなり効率よく実行されます。 このような手法はインラインメソッドキャッシュ(inline method cache)と呼ばれています。
注意: 内部的な話なので、C# のバージョンアップで実装方法が変わる可能性もあります(基本的な原理は変わらないと思いますが)。 (今このページに書かれている内容は、C# 4.0 の時点の実装に基づいています。)
ポイント
-
dynamic 型ってのは実はなくて、内部的には object。
-
CallSite クラスを使って動的コード生成。
動的 callsite
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 型ってのは本当はなくて、内部的には単なる objetct 型になる。 メソッドの引数が dynamic 型の場合、[Dynamic] 属性が付く。
-
CallSite 初期化時: どうやって動的コード生成するかを指定する。 CallSiteBinder(この例の場合、CSharpInvokeMemberBinder や CSharpGetMemberBinder がこのクラスを継承してる)を使う。
-
CallSite.Target: 動的に生成したコードの入っているデリゲート。 obj の中身がまだコード生成していない型だった場合、動的コード生成して Target デリゲート自身の中身を書きかえる。
Dynamic 属性
前述の通り、コンパイル結果的には 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 属性が付きます。
CallSiteBinder
続いて 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 の中の挙動としては)、以下のような動的コード生成を行います。
-
IDynamicObject インターフェースを実装した型の場合、TryGetMember などのメソッド呼び出し
-
COM オブジェクトの場合、COM Interop コード
-
その他の場合、「リフレクション」を使ってメンバーを持っているかどうか調べて、持っているならそのメンバーにアクセスするコードを生成。
CallSite.Target 内での動的コード生成
最後に、実際に動的コード生成。 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 を呼べば、 そこそこいい実行速度が得られます。
実行速度に関してもう少し詳しく言うと、
-
初回(今まで使ったことのない型のインスタンスで GetX を呼んだりしたとき)には動的コード生成(Target 内を更新)する分、かなり重たい処理が入る。
-
2回目以降は、そこそこ高速。
-
静的に
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(dynamic)
はそもそもエラーになります。
typeof(object)
が得られたりはしません。
dynamic の ToString 呼び出し
dynamic の実体は object なわけですが、 じゃあ、ToString() や GetHashCode() 等の object 型のメソッドはどうなるかというと・・・、 CallSite を介した動的コード生成になります。 ToString() だけ特別扱いされて静的なコードになったりはしません。
Dynamic 属性を直接使う
dynamic キーワードを使わず、 Dynamic 属性を直接付けようとするとコンパイルエラーになります。 「DynamicAttribute は直接は使えない。dynamic キーワードを使ってくれ」というような感じで怒られます。