C# は静的型付け言語です。
静的型付けというのは、コンパイル時に型やメソッドの構造が(ある程度)決定している、ということですね。
一方、JavaScript や Python 、Ruby などは動的型付け言語です。
動的型付けは、実行時に型やメソッドの構造が決まるということですね。
今回は、dynamic
と System.Dynamic
を使用した、C# における動的型付けについて見てゆきたいと思います。
そして最後に、デモとしてちょっと変わったコードをお見せいたします。
目次
-
dynamic
変数とリフレクション ExpandoObject
DynamicObject
- デモ(DSL 風 XML ジェネレータ)
dynamic
変数とリフレクション
dynamic
はクラスやインターフェイスなどの型の名前ではありません。
「変数」の型です(実体は object
型です)。
dynamic
変数は、実行時にメンバへのアクセスや演算を行なったとき、動的にコードを生成します。
これにより、異なる言語や実行環境の間で違いを吸収するなど、相互運用が可能になります。
このあたりの概要は、下記のページが参考になるかと思います。
dynamic
http://ufcpp.net/study/csharp/sp4_dynamic.htmldynamic の内部実装
http://ufcpp.net/study/csharp/sp4_callsite.html
相互運用だけでなく、リフレクションを参照する機能もあります。
こんな記述が可能です。
using System;
namespace ConsoleSample
{
class A
{
public int X { get; } = 10;
}
class Program
{
static void Main(string[] args)
{
dynamic a;
if (DateTime.Now.Ticks % 2 == 0)
{
a = new A();
}
else
{
a = new { X = 20 };
}
Console.WriteLine(a.X); // 10 or 20
}
}
}
変数 a
に異なる型を代入し、その後に同名のプロパティ X
を参照しています。
X が存在するかどうかは実行時に評価されるため、コンパイルエラーにはなりません。
少しだけ、動的型付けっぽいですね。
しかし、存在していないプロパティ(例えば Y
)にアクセスすると RuntimeBinderException
が発生してしまいます。
もっとスクリプト言語みたいに、宣言なしに自由に代入できないものでしょうか。
ExpandoObject
ExpandoObject
は、System.Dynamic
名前空間のクラスです。
dynamic
変数に代入して使います。
下記コードをご覧ください。
using System;
using System.Dynamic;
namespace ConsoleSample
{
class Program
{
static void Main(string[] args)
{
dynamic exp = new ExpandoObject();
exp.MyValue = 10;
exp.AnotherValue = 20;
exp.MyFunction = (Func<int, int, int>)((x, y) => x + y);
Console.WriteLine(exp.MyFunction(exp.MyValue, exp.AnotherValue)); // 30
}
}
}
なんと、任意のプロパティ名を自由に使用できてしまっています。
C# でもこんなことができてしまうんですね。
ちなみに、ExpandoObject
は前章のようにリフレクションを使用しているわけではありません。
DynamicObject
DynamicObject
も System.Dynamic
名前空間のクラスです。
こちらは継承して使います。
DynamicObject
を継承したクラスは、dynamic
の動的コード生成時に呼び出されるメソッド(TryXXXXメソッド群)をオーバーライドすることが可能です。
どんなものがあるか見てみましょう。
- キャスト(
TryConvert
メソッド) - 単項演算(
TryUnaryOperation
メソッド) - 二項演算(
TryBinaryOperation
メソッド) - プロパティの get(
TryGetMember
メソッド) - プロパティの set(
TrySetMember
メソッド) - インデクサの get(
TryGetIndex
メソッド) - インデクサの set(
TrySetIndex
メソッド) - メンバ呼び出し(
TryInvokeMember
メソッド) - 自身に () をつけて呼び出し(
TryInvoke
メソッド)
これらを実際にオーバーライドして使用した例が以下のコードです。
using System;
using System.Dynamic;
using System.Linq;
using System.Linq.Expressions;
namespace ConsoleSample
{
class MyDynamic : DynamicObject
{
int _x;
public MyDynamic(int x)
{
_x = x;
}
// キャスト
public override bool TryConvert(ConvertBinder binder, out object result)
{
Console.WriteLine($"{nameof(TryConvert)} : {binder.Type}");
if (binder.Type != typeof(int))
{
return base.TryConvert(binder, out result);
}
result = _x;
return true;
}
// 単項演算
public override bool TryUnaryOperation(UnaryOperationBinder binder, out object result)
{
Console.WriteLine($"{nameof(TryUnaryOperation)} : {binder.Operation}");
switch (binder.Operation)
{
case ExpressionType.UnaryPlus:
result = _x;
return true;
default:
return base.TryUnaryOperation(binder, out result);
}
}
// 二項演算
public override bool TryBinaryOperation(BinaryOperationBinder binder, object arg, out object result)
{
Console.WriteLine($"{nameof(TryBinaryOperation)} : {binder.Operation}");
switch (binder.Operation)
{
case ExpressionType.Add:
result = _x + (arg as MyDynamic)?._x ?? 0;
return true;
default:
return base.TryBinaryOperation(binder, arg, out result);
}
}
// プロパティの get
public override bool TryGetMember(GetMemberBinder binder, out object result)
{
Console.WriteLine($"{nameof(TryGetMember)} : {binder.Name}");
switch (binder.Name)
{
case "X":
result = _x;
return true;
default:
return base.TryGetMember(binder, out result);
}
}
// プロパティの set
public override bool TrySetMember(SetMemberBinder binder, object value)
{
Console.WriteLine($"{nameof(TrySetMember)} : {binder.Name} = {value}");
switch (binder.Name)
{
case "X":
_x = value is int ? (int)value : 0;
return true;
default:
return base.TrySetMember(binder, value);
}
}
// インデクサの get
public override bool TryGetIndex(GetIndexBinder binder, object[] indexes, out object result)
{
Console.WriteLine($"{nameof(TryGetIndex)} : [{indexes.Select(x => x.ToString()).Aggregate((x, y) => $"{x}, {y}")}]");
if (!indexes.All(x => x is int))
{
return base.TryGetIndex(binder, indexes, out result);
}
result = indexes.Cast<int>().Aggregate((x, y) => x + y);
return true;
}
// インデクサの set
public override bool TrySetIndex(SetIndexBinder binder, object[] indexes, object value)
{
Console.WriteLine($"{nameof(TrySetIndex)} : [{indexes.Select(x => x.ToString()).Aggregate((x, y) => $"{x}, {y}")}] = {value}");
if (!indexes.All(x => x is int) && value is int)
{
return base.TrySetIndex(binder, indexes, value);
}
_x = indexes.Cast<int>().Aggregate((x, y) => x + y) + (int)value;
return true;
}
// メンバ呼び出し
public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result)
{
Console.WriteLine($"{nameof(TryInvokeMember)} : {binder.Name}({(args.Length == 0 ? "" : args.Select(x => x.ToString()).Aggregate((x, y) => $"{x}, {y}"))})");
if (!args.All(x => x is string))
{
return base.TryInvokeMember(binder, args, out result);
}
result = args.Cast<string>().Aggregate((x, y) => x + y);
return true;
}
// 自身に () をつけて呼び出し
public override bool TryInvoke(InvokeBinder binder, object[] args, out object result)
{
Console.WriteLine($"{nameof(TryInvoke)} : ({(args.Length == 0 ? "" : args.Select(x => x.ToString()).Aggregate((x, y) => $"{x}, {y}"))})");
result = null;
return true;
}
}
class Program
{
static void Main(string[] args)
{
dynamic my = new MyDynamic(100);
Console.WriteLine((int)my);
Console.WriteLine(+my);
Console.WriteLine(my + my);
my.X = 300;
Console.WriteLine(my.X);
Console.WriteLine(my[1, 2, 3]);
my[10, 20] = 30;
Console.WriteLine(my.X);
Console.WriteLine(my.Func("A", "B", "C"));
my();
Console.ReadLine();
}
}
}
Ruby における method_missing
と似たような機能ですね。
特に、プロパティとメンバメソッドの名前を自由に決定できるのは、強力な気がします。
そんな機能を使い、次章ではものの役には立たないけれど、なんか変わったコーディングをしてみたいと思います。
デモ(DSL 風 XML ジェネレータ)
前章の内容を踏まえ、サンプルプログラムを作ります。
dynamic
を利用して XML を生成するクラスです。
実用性はありませんので、ご了承ください。
DSL(ドメイン固有言語)とは、汎用性または公共性の無い、狭い利用に特化した言語のことです。
下記が実行例です。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Xml.Linq;
namespace DynamicObjectSample
{
class Program
{
static void Main(string[] args)
{
dynamic xml = new DynamicXml();
var document =
xml(
xml.user * Tuple.Create("id", "1234") <<
(xml.name << xml.first_name * "太郎" + xml.last_name * "ダイナミック") +
xml.address * "ドットネット県シーシャープ市" +
xml.e_mail * "dynamic@mail.example.com"
).ToXDocument();
Console.WriteLine(document);
Console.ReadLine();
}
}
}
<user id="1234">
<name>
<first_name>太郎</first_name>
<last_name>ダイナミック</last_name>
</name>
<address>ドットネット県シーシャープ市</address>
<e_mail>dynamic@mail.example.com</e_mail>
</user>
dynamic
を使わなくても、演算子のオーバーロードで簡単に実現できそうな内容ですが、DynamicObject
の TryGetMember
を利用して任意のタグ名を指定している点にご注目ください。
Ruby ほどスマートには書けませんが、静的型付け言語の C# でこういったコードが書けるというのは、少し面白い気がします。
以下はサンプルコードです。
まず、XML ドキュメントを表すクラスです。
using System;
using System.Collections.Generic;
using System.Dynamic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Xml.Linq;
namespace DynamicObjectSample
{
class DynamicXml : DynamicObject
{
public DynamicXmlElement Root { get; private set; } = null;
public override bool TryGetMember(GetMemberBinder binder, out object result)
{
result = new DynamicXmlElement(binder.Name);
return true;
}
public override bool TryInvoke(InvokeBinder binder, object[] args, out object result)
{
if (args.Length == 1 && args[0] is DynamicXmlElement)
{
Root = (DynamicXmlElement)args[0];
result = this;
return true;
}
else
{
return base.TryInvoke(binder, args, out result);
}
}
public XDocument ToXDocument()
{
var document = new XDocument(new XDeclaration("1.0", "UTF-8", "yes"));
if (Root == null)
{
return document;
}
document.Add(Root.ToXElement());
return document;
}
}
}
次に、XML 要素を表すクラスです。
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Dynamic;
using System.Linq;
using System.Linq.Expressions;
using System.Text;
using System.Threading.Tasks;
using System.Xml.Linq;
namespace DynamicObjectSample
{
class DynamicXmlElement : DynamicObject
{
public string Name { get; }
Dictionary<string, string> _attributes = new Dictionary<string, string>();
public IReadOnlyDictionary<string, string> Attributes { get; }
List<DynamicXmlElement> _children = new List<DynamicXmlElement>();
public IReadOnlyList<DynamicXmlElement> Children { get; }
public string Text { get; private set; } = "";
DynamicXmlElement _next = null;
public DynamicXmlElement(string name)
{
if (name == null)
{
throw new ArgumentNullException(nameof(name));
}
Name = name;
Attributes = new ReadOnlyDictionary<string, string>(_attributes);
Children = new ReadOnlyCollection<DynamicXmlElement>(_children);
}
public override bool TryBinaryOperation(BinaryOperationBinder binder, object arg, out object result)
{
switch (binder.Operation)
{
case ExpressionType.LeftShift:
if (arg is DynamicXmlElement)
{
var child = (DynamicXmlElement)arg;
_children.Add(child);
result = this;
return true;
}
else
{
return base.TryBinaryOperation(binder, arg, out result);
}
case ExpressionType.Multiply:
if (arg is Tuple<string, string>)
{
var attr = (Tuple<string, string>)arg;
_attributes[attr.Item1] = attr.Item2;
result = this;
return true;
}
else if (arg is string)
{
Text = (string)arg;
result = this;
return true;
}
else
{
return base.TryBinaryOperation(binder, arg, out result);
}
case ExpressionType.Add:
if (arg is DynamicXmlElement)
{
GetBrothers().Last()._next = (DynamicXmlElement)arg;
result = this;
return true;
}
else
{
return base.TryBinaryOperation(binder, arg, out result);
}
default:
return base.TryBinaryOperation(binder, arg, out result);
}
}
public XElement ToXElement()
{
var element = new XElement(Name);
element.Add(_attributes.Select(pair => new XAttribute(pair.Key, pair.Value)).ToArray());
element.Add(Text);
element.Add(_children.SelectMany(elms => elms.GetBrothers()).Select(elm => elm.ToXElement()).ToArray());
return element;
}
IEnumerable<DynamicXmlElement> GetBrothers()
{
DynamicXmlElement last;
for (last = this; last != null; last = last._next)
{
yield return last;
}
}
}
}
おわりに
dynamic
変数、 ExpandoObject
、DynamicObject
について、簡単にですがご紹介いたしました。
こういった柔軟性を持っているところも、C# の魅力の 1 つではないでしょうか。