LoginSignup
25
33

More than 5 years have passed since last update.

動的型付け言語 C#

Posted at

C# は静的型付け言語です。
静的型付けというのは、コンパイル時に型やメソッドの構造が(ある程度)決定している、ということですね。
一方、JavaScript や Python 、Ruby などは動的型付け言語です。
動的型付けは、実行時に型やメソッドの構造が決まるということですね。

今回は、dynamicSystem.Dynamic を使用した、C# における動的型付けについて見てゆきたいと思います。
そして最後に、デモとしてちょっと変わったコードをお見せいたします。

目次

  • dynamic 変数とリフレクション
  • ExpandoObject
  • DynamicObject
  • デモ(DSL 風 XML ジェネレータ)

dynamic 変数とリフレクション

dynamic はクラスやインターフェイスなどの型の名前ではありません。
「変数」の型です(実体は object 型です)。
dynamic 変数は、実行時にメンバへのアクセスや演算を行なったとき、動的にコードを生成します。
これにより、異なる言語や実行環境の間で違いを吸収するなど、相互運用が可能になります。
このあたりの概要は、下記のページが参考になるかと思います。

dynamic
http://ufcpp.net/study/csharp/sp4_dynamic.html

dynamic の内部実装
http://ufcpp.net/study/csharp/sp4_callsite.html

相互運用だけでなく、リフレクションを参照する機能もあります。
こんな記述が可能です。

Sample.cs
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 変数に代入して使います。
下記コードをご覧ください。

Sample.cs
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

DynamicObjectSystem.Dynamic 名前空間のクラスです。
こちらは継承して使います。
DynamicObject を継承したクラスは、dynamic の動的コード生成時に呼び出されるメソッド(TryXXXXメソッド群)をオーバーライドすることが可能です。
どんなものがあるか見てみましょう。

  • キャスト(TryConvert メソッド)
  • 単項演算(TryUnaryOperation メソッド)
  • 二項演算(TryBinaryOperation メソッド)
  • プロパティの get(TryGetMember メソッド)
  • プロパティの set(TrySetMember メソッド)
  • インデクサの get(TryGetIndex メソッド)
  • インデクサの set(TrySetIndex メソッド)
  • メンバ呼び出し(TryInvokeMember メソッド)
  • 自身に () をつけて呼び出し(TryInvoke メソッド)

これらを実際にオーバーライドして使用した例が以下のコードです。

Sample.cs
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(ドメイン固有言語)とは、汎用性または公共性の無い、狭い利用に特化した言語のことです。

下記が実行例です。

Program.cs
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();
        }
    }
}
Output
<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 を使わなくても、演算子のオーバーロードで簡単に実現できそうな内容ですが、DynamicObjectTryGetMember を利用して任意のタグ名を指定している点にご注目ください。
Ruby ほどスマートには書けませんが、静的型付け言語の C# でこういったコードが書けるというのは、少し面白い気がします。

以下はサンプルコードです。
まず、XML ドキュメントを表すクラスです。

DynamicXml.cs
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 要素を表すクラスです。

DynamicXmlElement.cs
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 変数、 ExpandoObjectDynamicObject について、簡単にですがご紹介いたしました。
こういった柔軟性を持っているところも、C# の魅力の 1 つではないでしょうか。

25
33
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
25
33