【C#】 dynamic型の裏側

# 言語機能`dynamic`型

C#では通常、コンパイル時にすべての呼び出しがチェックされ、無効なものがあればコンパイルエラーとなります。
例えば、object型には`ToString`メソッドが含まれますが、`GetString`のように書き間違えてしまった場合などはこのチェックによってコンパイル時に発見することができます。
一方でJavaScriptやPythonのような動的な言語では、コンパイル時に呼び出しが間違っていてもスルーされてしまいます。というかそもそもコンパイル言語ではないですが…（IEDなどではエラー表示になるかもしれませんがそれとは関係なく、「実行」することはできてしまします）

これはC#をはじめ静的な言語が安全といわれる要素の一つであり、非常に有難い機能なのですが、時にこれが邪魔になってしまうことがあります。

```csharp
// 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`型を使うと、（見かけ上）コンパイル時のチェックをスルーすることができます。

```csharp
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変数へアクセスするとコンパイラにより、以下のような変換が行われます。

```csharp
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](https://github.com/microsoft/referencesource/blob/5697c29004a34d80acdaf5742d7e699022c64ecd/Microsoft.CSharp/Microsoft/CSharp/Binder.cs#L156) [CallSite.cs#L192](https://referencesource.microsoft.com/#System.Core/Microsoft/Scripting/Actions/CallSite.cs,192)

`CallSite<TFunc>.Create` では、`CreateCustomUpdateDelegate`が呼び出され、Expression APIでDelegateのILコードが動的に生成されキャッシュされます。どのようなコードに対応するデリゲートを生成しているかは、コメントに記載されています。
[CallSite.cs#L358](https://referencesource.microsoft.com/#System.Core/Microsoft/Scripting/Actions/CallSite.cs,358)

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

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

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

ここで出てくる`this.Binder`が、★マークの`binder`になります。そしてこれはRuntimeが提供するNativeのAPIのようです。
Dynamicが呼び出し部分のコードをキャッシュしてくれるというのは「この先（C++）でキャッシュされている」ということでしょう。
該当のCppコードは[こちら](https://github.com/dotnet/runtime/blob/69e114c1abf91241a0eeecf1ecceab4711b8aa62/src/coreclr/vm/dispatchinfo.cpp#L1226)になると思われます。（本当にキャッシュする処理が入っているかは未確認）
[Binder.cs,22](https://referencesource.microsoft.com/#Microsoft.CSharp/Microsoft/CSharp/RuntimeBinder/Binder.cs,22)


# カスタマイズされた`dynamic変数`（`DynamicObject`）

C#のdynamic変数は、中身のオブジェクトが特定の型やインターフェースを実装していることで任意のふるまいをさせることができます。
詳しくは[このPDF](https://download.microsoft.com/download/5/4/B/54B83DFE-D7AA-4155-9687-B0CF58FF65D7/implementing-dynamic-interfaces.pdf)にかかれています。

`DynamicObject`と`IDynamicMetaObjectProvider`というのがあるのですが、単純な`DynamicObject`についてみてみましょう。

### コンパイル結果

デフォルトと同じです。

```csharp
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を作ることができます。（[実装例](https://github.com/wallstudio/PowerAccessor/blob/master/PowerAccessor.cs)）

# まとめ

dynamicの内部実装と、カスタマイズの仕方を見てみました。

とはいえ、コンパイラチェックを回避してしまうのでIDEの支援をうけられない、実行するまでtypoに気が付けないなどのデメリットがあります。
使うべきところはテストや普段書き換えが発生しないようなコードに限定するべきで、できるだけインターフェイスやコード生成で代替しておくのがベターだと思います。

# 参考

* [DynamicObject クラス](https://docs.microsoft.com/ja-jp/dotnet/api/system.dynamic.dynamicobject?view=net-5.0)
* [IDynamicMetaObjectProvider インターフェイス](https://docs.microsoft.com/ja-jp/dotnet/api/system.dynamic.idynamicmetaobjectprovider?view=net-5.0)
* [Implementing Dynamic Interfaces](https://download.microsoft.com/download/5/4/B/54B83DFE-D7AA-4155-9687-B0CF58FF65D7/implementing-dynamic-interfaces.pdf)
* [Expressionを使って動的コード生成するための逆引きメモ](https://qiita.com/edo_m18/items/cab991b3c3ed667c4c35)
* [wallstudio / PowerAccessor](https://github.com/wallstudio/PowerAccessor/blob/master/PowerAccessor.cs)
* [dotnet / runtime](https://github.com/dotnet/runtime)