読者です 読者をやめる 読者になる 読者になる

An implementation of duck-typing by anonymous types

.NET

C# 3.0 の新シンタックス悪用講座と化しつつありますが.

Anonymous Types にデリゲートを持たせる

Anonymous Types の要素にデリゲートを持たせると,シンタックス上はメソッドに見えるため,あたかも匿名のインターフェイスのように使うことができます.こいつを使って duck typing のテスト.
とりあえずこんな感じのコードが動いています.

public class Duck
{
    public void Bark()
    {
        Console.WriteLine("gaaa");
    }
}
public class Human
{
    public void Bark()
    {
        Console.WriteLine("hauhau");
    }
}

static class Program
{
    static void Main(string[] args)
    {
        var barkable = DuckType.Define( () => new { Bark = default(Action) } );

        new object[] { new Duck(), new Human() }
            .ToList()
            .ForEach(o => barkable[o].Bark());
    }
}

特徴

  • リフレクションを使用した,実行時解決です.
  • マッピング処理は実行時コード生成を行っています.一度キャッシュされてしまえばそこまで遅いというものでもありません.
  • 今のところメソッドしかキャプチャできません.プロパティは対応可 (未実装).フィールドはコピーになるので微妙.
  • 半分気付きつつ書いてましたが,無理に anonymous types 単位でキャプチャせずに,メソッド単位でデリゲートにキャプチャする方が現実的でかつ簡単だと思います.そっちの実装はまた今度.

実装

以下本体.全然整理できていないひどい状態ですが.後で書き直します.

using System;
using System.Collections.Generic;
using System.Reflection;
using System.Linq;
using System.Linq.Expressions;

public static class DuckType
{
    public class DuckTypeImpl<TDest>
        where TDest : class
    {
        private readonly ConstructorInfo _destConstructor;
        private readonly MemberInfo[] _destConstructorBindings;
        public DuckTypeImpl(ConstructorInfo constructor, MemberInfo[] bindings)
        {
            _destConstructor = constructor;
            _destConstructorBindings = bindings;
        }

        public TDest this[object source]
        {
            get
            {
                return BinderCache.GetConverter(source.GetType(), _destConstructor, _destConstructorBindings)(source);
            }
        }
        private static class BinderCache
        {
            private static readonly Dictionary<Type, Func<object, TDest>> _converterCache = new Dictionary<Type, Func<object, TDest>>();
            public static Func<object, TDest> GetConverter(Type sourceType, ConstructorInfo constructor, MemberInfo[] bindings)
            {
                Func<object, TDest> delg;
                if (_converterCache.TryGetValue(sourceType, out delg))
                {
                    return delg;
                }
                delg = typeof(BinderCache).GetMethod("GetBinder").MakeGenericMethod(new[] { sourceType }).Invoke(null, new object[] { constructor, bindings }) as Func<object, TDest>;
                _converterCache[sourceType] = delg;
                return delg;
            }
            public static Func<object, TDest> GetBinder<TSource>(ConstructorInfo constructor, MemberInfo[] bindings) where TSource : class
            {
                var constructorParams = constructor.GetParameters();
                var fieldtypes = bindings.Cast<MethodInfo>().Select(p => p.ReturnType).ToArray();
                var param = Expression.Parameter(typeof(TSource), "source");
                var createDelegate = typeof(Delegate).GetMethod("CreateDelegate", new[] { typeof(Type), typeof(object), typeof(MethodInfo) });
                var expr = Expression.Lambda<Func<TSource, TDest>>(
                        Expression.New(
                            constructor,
                            (
                                from constructorParam in constructorParams
                                let fieldtype = constructorParam.ParameterType
                                let sourceMethod = typeof(TSource).GetMethod(constructorParam.Name)
                                select Expression.Convert
                                       (
                                           Expression.Call
                                           (
                                               createDelegate,
                                               Expression.Constant(fieldtype),
                                               param,
                                               Expression.Constant(sourceMethod)
                                           ),
                                           fieldtype
                                       )
                            ).ToArray()
                        ),
                        param
                    );
                return o => expr.Compile()((TSource)o);
            }
        }

    }

    public static DuckTypeImpl<TDest> Define<TDest>(Expression<Func<TDest>> expr) where TDest : class
    {
        if (expr.Body.NodeType == ExpressionType.New)
        {
            var newExpr = expr.Body as NewExpression;
            if (newExpr.Members.Count > 0)
            {
                return new DuckTypeImpl<TDest>(newExpr.Constructor, newExpr.Members.ToArray());
            }
        }
        return null;
    }
}

更新履歴

  • 2008年7月4日
    • Anonymous Type のコンストラクタ引数と対応パラメータの関連付けに,NewExpression.Members を使うようにした