この記事はC# Advent Calendar 2017の13日目の記事デスよっと。
前口上
動的なマッピングなんかするときに威力を発揮するDynamicObjectですが、だいたいが動的に解決されるので挙動にわりかし癖がある。
なので、今回は割と頻用するであろう、TryGetMember
とTryConvert
を主に検証していこうかなと思いますのでお付き合い頂ければ幸い。
DynamicObjectとは何なの?
動的なアクセスやらの解決をしたい際、一般的なバインドでは無く、DynamicMetaObjectBinder
経由で一風変わったコールサイトの構築をしてくれたりする。
利用する際は、DynamicObjectから継承して必要なメソッドをオーバーライドすることでカスタムロジックを噛ませることが可能になったりする。
TryGetMemberで出来ること
例えば、SomeDynamic.HogeMoge
という感じでアクセスしたい。ただ、HogeMoge
はバックエンドの状態でFooBar
など任意の名前になるよみたいなとき、TryGetMember
メソッドをオーバーライドすることで実行時に適宜ディスパッチすることが出来る。
サンプルとして、以下のようなDynamicObject
を継承したクラスをこさえる
class TryGetMemberSample : DynamicObject
{
public override bool TryGetMember(GetMemberBinder binder, out object result)
{
if (binder.Name == "HogePiyo")
{
result = 42;
}
else if (binder.Name == "FooBar")
{
result = 114514;
}
else
{
result = 0;
}
return true;
}
}
binder.Name
に、実行時どのような識別子で呼ばれたかが入っているので、今回は決め打ちで返す値を変えているけど、実際には内部辞書とのKeyあたりと突合して有無を調べるみたいなのが定石かなと。
また、binder.ReturnType
が有るので、一見代入先の型を取得可能と思えるけど、ここは常にobject
になってるので、当てにならないし出来ない。
実際、代入する型に合わせてカスタムロジックを仕込みたいのなら、後述するTryConvert
を使う必要がある。
さて、少しサンプルを書き換えて、以下のようにした。
class TryGetMemberSample : DynamicObject
{
public int Hoge => 10;
public string Piyo=>"hello";
public override bool TryGetMember(GetMemberBinder binder, out object result)
{
if (binder.Name == "HogePiyo")
{
result = 42;
}
else if (binder.Name == "FooBar")
{
result = 114514;
}
else
{
result = 0;
}
return true;
}
}
この場合、
private static void Main(string[] args)
{
var d = (dynamic) new TryGetMemberSample();
int i = d.Hoge;
i = d.HogePiyo;
i=d.Piyo;
}
このような呼び出しを行ったとき、
最初のd.Hoge
の呼び出しは、TryGetMember
を経由せず、直接Hoge
プロパティにアクセスするので注意が必要となる。
Hoge
の場合は、型が一致してるので、問題ないけど、Piyo
の場合、string
になってるのでコレはどうなるかというと、型の不一致から、TryGetMember
が呼ばれるわけでは無く、RuntimeBinderException
が飛んでくるので、この辺は注意が必要。
また、TryGetMemberが実行時に解決できない場合は、falseを返すことで、RuntimeBinderException
を発生させることが出来る。
個人的には、TryMember
で名前経由で値をディスパッチするクラスと、TryConvert
で型をディスパッチするクラスを別にして、二段構えにする都割と良い感じかなと。
TryConvertで出来ること
さて、先のTryGetMember
では、メンバ名を元にしたディスパッチが可能であることをみてきた。
ここでは、型を元に実行時に変換処理のカスタムロジックを書く方法をみていこう。同様に、以下のようなクラスを作った
class TryConvertSample : DynamicObject
{
public override bool TryConvert(ConvertBinder binder, out object result)
{
if (binder.Type == typeof(int))
{
result = 42;
return true;
}
if (binder.Type == typeof(double))
{
result = 42.195;
return true;
}
if (binder.Type == typeof(string))
{
result = "hello world";
return true;
}
result = default;
return false;
}
}
binder.Type
には、変換先の型が入っているので、こいつを元にして任意の変換を噛ませることが可能となる。
また、binder.Explicit
には明示的に呼ばれたのならtrueそれ以外はfalseになるので、明示的なのか暗黙なのかで挙動を変化させることも可能。
TryConvert
は非常に強力なカスタムロジックを仕込める反面、結構注意しないと色々とハマる部分も有るので、以下その辺のまとめ。
implicit/explicit operatorとの兼ね合い
先のTryGetValu
と同様、明示的に書かれた方が優先される。
但し、explicit operatorが存在する場合は、implicitで読んだ場合は、TryConvertへ、explicitで読んだときはexplicit operatorへとディスパッチされ、逆にimplicit operatorが存在する場合は、explicitに呼んだ場合はTryConvertへ、implicitに呼んだときはimplicit operatorへとディスパッチされるので、この辺注意のほど。
T?の取り扱い
変換演算子を使った場合は、int
なら、int?
へも適用可能となりますが、先のサンプルのような場合は、厳密に型で一致させてるので、変換が出来なくなります。従って、Nullableにも対応させたいなら、それ相応の条件式を用意する必要がある。
変換先がinterfaceの場合
変換先がinterfaceの場合は、結構癖があるというか、明示的に呼ぶのか、暗黙で呼ぶのかでTryConvert
メソッドが呼ばれるか否か変化することになる。
この点に関しては、先日ここら辺に書いているので、参考にしてもらえれば幸い。
TryGetMemberとTryConvertは単一のクラスでは実現不可能
このように、TryGetMemberではどのような型として評価されるのか判断できず、TryConvertによって初めて評価されると言うことがわかった。
なので、jsonやRDBMSへの動的マッピングにDynamicObjectを利用する際は、TryGetMember
を担当するクラスと、TryConvert
を担当するクラスを分けるとすっきり表現できる。
最後のまとめとして、その簡単なサンプルを提示して、検証していこう。
using System;
using System.Collections.Generic;
using System.Dynamic;
namespace DynamicSample
{
internal delegate bool CustomConverter(object convertFrom, out object convertTo, out Type convertToType);
internal class DynamicValue : DynamicObject
{
private readonly CustomConverter _converter;
private readonly object _value;
public DynamicValue(object value, CustomConverter converter)
{
_value = value;
_converter = converter;
}
public override bool TryConvert(ConvertBinder binder, out object result)
{
if (_converter(_value, out var to, out var type))
if (binder.Type.IsAssignableFrom(type))
{
result = to;
return true;
}
result = default;
return false;
}
public static implicit operator int(DynamicValue value)
{
return (int) value._value;
}
public static implicit operator double(DynamicValue value)
{
return (double) value._value;
}
public static implicit operator string(DynamicValue value)
{
return (string) value._value;
}
}
internal class MemberResolver : DynamicObject
{
private readonly IDictionary<string, object> _data;
public MemberResolver(IDictionary<string, object> data)
{
_data = data;
}
public override bool TryGetMember(GetMemberBinder binder, out object result)
{
bool timeConverter(object from, out object to, out Type type)
{
if (from is long l)
{
to = new DateTime(l);
type = typeof(DateTime);
return true;
}
to = default;
type = default;
return false;
}
if (_data.TryGetValue(binder.Name, out result))
{
if (binder.Name.Contains("Time") && result is long)
result = new DynamicValue(result, timeConverter);
else
result = new DynamicValue(result, null);
return true;
}
return false;
}
}
internal class Program
{
private static void Main(string[] args)
{
var data = new Dictionary<string, object>
{
["SomeTime"] = DateTime.Now.Ticks,
["Hoge"] = 114514,
["Bar"] = "hello world"
};
dynamic resolver = new MemberResolver(data);
DateTime time = resolver.SomeTime;
Console.WriteLine(time);
int num = resolver.Hoge;
Console.WriteLine(num);
Console.WriteLine((string) resolver.Bar);
}
}
}
TryConvert
ですべて解決しようとすると、メソッドのサイズが肥大化するのと、パフォーマンス的にもあんまり良くないから、想定される変換は変換演算子を利用して、別途カスタムコンバータが存在するときのみTryConvertで解決するような形を取れば、悪くないかなと言うことでまとめてみた。
まとめ
dynamicは文字通り、実行時までバインドを遅延させることから、柔軟性がある反面、予想外の挙動を取ることが多く一般的なC#のコンテキストとは又別の注意がそれなりに必要だと思う。
同様に、DynamicObjectから派生したクラスは高度な柔軟性を持ち合わせている反面、特にTryConvertの挙動は一般的なC#のコンテキストが優先されることから思った通りの挙動をしてくれない危険性が常に存在することになるかなって。
今回の検証でそこら辺の解決の一助になってくれればと思いまとめてみました。
それでは早いですが、メリークリスマス!