言語機能dynamic
型
C#では通常、コンパイル時にすべての呼び出しがチェックされ、無効なものがあればコンパイルエラーとなります。
例えば、object型にはToString
メソッドが含まれますが、GetString
のように書き間違えてしまった場合などはこのチェックによってコンパイル時に発見することができます。
一方でJavaScriptやPythonのような動的な言語では、コンパイル時に呼び出しが間違っていてもスルーされてしまいます。というかそもそもコンパイル言語ではないですが…(IEDなどではエラー表示になるかもしれませんがそれとは関係なく、「実行」することはできてしまします)
これはC#をはじめ静的な言語が安全といわれる要素の一つであり、非常に有難い機能なのですが、時にこれが邪魔になってしまうことがあります。
// Class AもBも同じ名前のメンバを持っていても、同一扱いすることはできない
// (或いは、Interfaceを付けるしかない)
class A { public string Text => "A"; }
class B { public string Text => "B"; }
void LogText(object A_Or_B) {
if(A_Or_B is A a) Log(a.Text);
else if(A_Or_B is B b) Log(b.Text);
}
// 実行してみないとメンバ名が分からない場合に都合が悪い
// 文字列でアクセスしても大して変わらないといえば変わらないが、普段の記法に馴染まない
object unknownObject = Json.Parse("{ 'a' : 100 }");
Log(unknownObject.a); // Compile NG
そこでdynamic
型を使うと、(見かけ上)コンパイル時のチェックをスルーすることができます。
void LogText(dynamic A_Or_B) => Log(A_Or_B.Text);
dynamic unknownObject = Json.Parse("{ 'a' : 100 }");
Log(unknownObject.a); // Compile OK
しかし、これはあくまでC#の機能であり、コンパイラが良しなに「文字列によるアクセス」に変換してくれているだけにすぎません。
具体的にどのような変換がなされ、プログラマはそれをどのように制御すればよいのか見ていきます。
デフォルトのdynamic変数
普通のオブジェクトインスタンス実体をdynamic変数に入れたときの挙動です。(実体がDynamicObject
の派生オブジェクトではない)
機能
機能としては、メンバの名前チェックをスルーし、実行時リフレクションによる文字列解決されます。メンバが特定されると、アクセス処理が動的に生成され(DLR)それでアクセスされるとのことです。(ちなみに、動的生成は最初の一度のみで、二回目からはキャッシュされるとのことです。)
コンパイル
dynamic変数へアクセスするとコンパイラにより、以下のような変換が行われます。
dynamic value = this;
value.Execute(0);
// ↓
using static Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfoFlags;
static CallSite<Func<CallSite, object, object>> DelegateCache;
object CS2ILCompileResult() {
if (Cache.Delegate == null) {
// ★
CallSiteBinder binder = Binder.InvokeMember(
flags: CSharpBinderFlags.None,
name: "Execute", // 名前解決!
typeArguments: null,
context: typeof(Test),
argumentInfo: new [] {
CSharpArgumentInfo.Create(None, null), // 0個目はダミーになる
CSharpArgumentInfo.Create(UseCompileTimeType|Constant, null);
});
DelegateCache= CallSite<Func<CallSite, object, int, object>>.Create(binder);
}
DelegateCache.Target(Cache.Delegate, this, 0);
}
Binder.InvokeMember
でCallSiteBinder
という、リフレクションでいうMemberInfo
のようなものを構築し、CallSite<TFunc>.Create
でデリゲート化しています。
Binder.cs#L156 CallSite.cs#L192
CallSite<TFunc>.Create
では、CreateCustomUpdateDelegate
が呼び出され、Expression APIでDelegateのILコードが動的に生成されキャッシュされます。どのようなコードに対応するデリゲートを生成しているかは、コメントに記載されています。
CallSite.cs#L358
二段構えのキャッシュになっているのと、最適化のためなのか分かりませんが結構読みづらい処理になっていますが、要約すると…
- L1キャッシュのキャッシュプールが
CallSiteOps.GetRules(this)
- L2キャッシュのキャッシュプールが
CallSiteOps.GetRuleCache(@this).GetRules()
- それぞれ、
CallSiteOps.GetMatch
を使ってヒットか否かを判定 - ヒットしなかった場合には、
this.Binder.BindDelegate(@this, args)
で新しいDelegateを生成
つまり、ここまでは前座ということで、実際の呼び出しの部分をどうするかはこの先ということになります。
ここで出てくるthis.Binder
が、★マークのbinder
になります。そしてこれはRuntimeが提供するNativeのAPIのようです。
Dynamicが呼び出し部分のコードをキャッシュしてくれるというのは「この先(C++)でキャッシュされている」ということでしょう。
該当のCppコードはこちらになると思われます。(本当にキャッシュする処理が入っているかは未確認)
Binder.cs,22
カスタマイズされたdynamic変数
(DynamicObject
)
C#のdynamic変数は、中身のオブジェクトが特定の型やインターフェースを実装していることで任意のふるまいをさせることができます。
詳しくはこのPDFにかかれています。
DynamicObject
とIDynamicMetaObjectProvider
というのがあるのですが、単純なDynamicObject
についてみてみましょう。
コンパイル結果
デフォルトと同じです。
class MyDynamic : DynamicObject {}
dynamic dy = new MyDynamic();
var m = dy.hoge;
// ↓
if (DelegateCache == null)
{
Type typeFromHandle = typeof(C);
CSharpArgumentInfo[] array = new CSharpArgumentInfo[1];
array[0] = CSharpArgumentInfo.Create(None, null);
DelegateCache = CallSite<Func<CallSite, object, object>>.Create(Binder.GetMember(None, "hoge", typeFromHandle, array));
}
m = DelegateCache.Target(DelegateCache, arg);
DynamicObject
のメンバ
Method | 説明 |
---|---|
GetDynamicMemberNames | メンバを列挙(いつ使われるのか不明) |
GetMetaObject | IDynamicMetaObjectProviderへの変換?(いつ使われるのか不明) |
TryBinaryOperation | 二項演算(四則演算や論理演算) |
TryConvert | キャスト |
TryCreateInstance | コンストラクタ(dynamicはnewできないのでこの場合不要) |
TryDeleteIndex | ?(いつ使われるのか不明) |
TryDeleteMember | ?(いつ使われるのか不明) |
TryGetIndex | インデクサによるアクセス([] ) |
TrySetIndex | インデクサによるアクセス([] ) |
TryGetMember | プロパティやフィールドへのアクセス |
TrySetMember | プロパティやフィールドへのアクセス |
TryInvoke | Delegateのようにそのまま呼び出し(((dynamic)obj)(args) ) |
TryInvokeMember | メソッドの呼び出し |
TryUnaryOperation | 単項演算(否定やビット反転など) |
これらが空の仮想メソッドとして実装されているので、必要なものについてオーバライドすることで任意のふるまいをさせることができます。
例えば、デフォルトのdynamicはプライベートメンバにアクセスすることはできませんが、上記をプライベートを貫通するリフレクションで実装しプライベート貫通式dynamicを作ることができます。(実装例)
まとめ
dynamicの内部実装と、カスタマイズの仕方を見てみました。
とはいえ、コンパイラチェックを回避してしまうのでIDEの支援をうけられない、実行するまでtypoに気が付けないなどのデメリットがあります。
使うべきところはテストや普段書き換えが発生しないようなコードに限定するべきで、できるだけインターフェイスやコード生成で代替しておくのがベターだと思います。