27
16

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

【C#】 dynamic型の裏側

Last updated at Posted at 2020-12-31

言語機能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.InvokeMemberCallSiteBinderという、リフレクションでいうMemberInfoのようなものを構築し、CallSite<TFunc>.Createでデリゲート化しています。
Binder.cs#L156 CallSite.cs#L192

CallSite<TFunc>.Create では、CreateCustomUpdateDelegateが呼び出され、Expression APIでDelegateのILコードが動的に生成されキャッシュされます。どのようなコードに対応するデリゲートを生成しているかは、コメントに記載されています。
CallSite.cs#L358

二段構えのキャッシュになっているのと、最適化のためなのか分かりませんが結構読みづらい処理になっていますが、要約すると…

  1. L1キャッシュのキャッシュプールが CallSiteOps.GetRules(this)
  2. L2キャッシュのキャッシュプールが CallSiteOps.GetRuleCache(@this).GetRules()
  3. それぞれ、CallSiteOps.GetMatch を使ってヒットか否かを判定
  4. ヒットしなかった場合には、this.Binder.BindDelegate(@this, args) で新しいDelegateを生成

つまり、ここまでは前座ということで、実際の呼び出しの部分をどうするかはこの先ということになります。

ここで出てくるthis.Binderが、★マークのbinderになります。そしてこれはRuntimeが提供するNativeのAPIのようです。
Dynamicが呼び出し部分のコードをキャッシュしてくれるというのは「この先(C++)でキャッシュされている」ということでしょう。
該当のCppコードはこちらになると思われます。(本当にキャッシュする処理が入っているかは未確認)
Binder.cs,22

カスタマイズされたdynamic変数DynamicObject

C#のdynamic変数は、中身のオブジェクトが特定の型やインターフェースを実装していることで任意のふるまいをさせることができます。
詳しくはこのPDFにかかれています。

DynamicObjectIDynamicMetaObjectProviderというのがあるのですが、単純な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に気が付けないなどのデメリットがあります。
使うべきところはテストや普段書き換えが発生しないようなコードに限定するべきで、できるだけインターフェイスやコード生成で代替しておくのがベターだと思います。

参考

27
16
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
27
16

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?