代码改变世界

Effective C# 学习笔记(四十一)使利用DynamicObject或IDynamicMetaObjectProvider接口实现数据驱动的动态类型

2011-08-02 17:18  小郝(Kaibo Hao)  阅读(2338)  评论(0编辑  收藏  举报

实现动态行为可以帮助解决你编程实践中遇到的各种挑战。当要创建动态类型时,首先判断是否可以通过集成System.Dynamic.DynamicObject来实现。若不能通过继承的方式实现,就要实现IDynamicMetaObjectProvider接口,通过创建继承自DynamicMetaObject类型的嵌套类并重载其对应方法来实现对你要创建的动态类型的属性访问、方法调用等各种动态行为。其中用到了较为复杂的Expression定义,这层抽象较难理解,也较容易出错,请注意这部分逻辑的覆写。另外,用你自己的动态逻辑也要考虑的性能损失的问题,因为动态编程本来就要比静态编程消耗更多的资源,有得必有失么:)

 

动态编程的一个强大的能力就是可以让你在运行时创建接口随环境改变的类型。在C#中你可以使用以下两种方法实现这个效果。

  1. 继承System.Dynamic.DynamicObject基类

首先,看一个例子,加入你有一个类型,其的属性(Properties)是在运行时动态添加的,你可以如下定义这个类型。代码如下:

class DynamicPropertyBag : DynamicObject

{

private Dictionary<string, object> storage = new Dictionary<string, object>();

public override bool TryGetMember(GetMemberBinder binder, out object result)

{

if (storage.ContainsKey(binder.Name))

{

result = storage[binder.Name];

return true;

}

result = null;

return false;

}

public override bool TrySetMember(SetMemberBinder binder, object value)

{

string key = binder.Name;

if (storage.ContainsKey(key))

storage[key] = value;

else

storage.Add(key, value);

return true;

}

public override string ToString()

{

StringWriter message = new StringWriter();

foreach (var item in  storage)

message.WriteLine("{0}:\t{1}", item.Key, item.Value);

return message.ToString();

}

}

 

//使用动态类型属性

dynamic dynamicProperties = new DynamicPropertyBag();

try

{

//尝试获取不存在的属性

Console.WriteLine(dynamicProperties.Marker);

}

catch (Microsoft.CSharp.RuntimeBinder.RuntimeBinderException)

{

Console.WriteLine("There are no properties");

}

//动态添加属性

dynamicProperties.Date = DateTime.Now;

dynamicProperties.Name = "Bill Wagner";

dynamicProperties.Title = "Effective C#";

dynamicProperties.Content = "Building a dynamic dictionary";

//输出动态属性

Console.WriteLine(dynamicProperties);

 

DynamicObject类型包含了类似TrySetMember的方法来处理索引、方法、构造器、一元二元运算的调用方法。你可以自己重载这些方法,来构建你自己的动态逻辑。你要遵守的原则是调用方法时要检查被调用的是哪个Binder对象,处理相关的逻辑。当有返回值时,或有输出变量时,对返回值或输出变量(out parameter)赋值。

 

再看一例,下面定义了一个行星的XML数据,其中有的行星有像月亮的卫星,而有的没有,这时你在处理这样的XML的时候便免不了做节点丢失,缺少属性的判断:

<Planets>

<Planet>

<Name>Mercury</Name>

</Planet>

<Planet>

<Name>Venus</Name>

</Planet>

<Planet>

<Name>Earth</Name>

<Moons>

<Moon>Moon</Moon>

</Moons>

</Planet>

<Planet>

<Name>Mars</Name>

<Moons>

<Moon>Phobos</Moon>

<Moon>Deimos</Moon>

</Moons>

</Planet>

<!-- other data elided -->

</Planets>

//Linq to XML操作上面的数据

// Create an XElement document containing

// solar system data:

var xml = createXML();

var firstPlanet = xml.Element("Planet");

var earth = xml.Elements("Planet").Skip(2).First();

var moon = xml.Elements("Planet").Skip(2).First().Elements("Moons").First().Element("Moon");

 

上面的代码获取某个节点值是如此的麻烦,而且并不直观。这其实是在处理一个数据驱动的业务模型,你可以通过继承DynamicObject类型来处理这样的XML数据操作,对于数据XML中不存在的节点DynamicElement返回空值。如下代码所示:

//创建一个继承自dynamicObject类型的Xml元素处理类型。

public class DynamicXElement : DynamicObject

{

private readonly XElement xmlSource;

public DynamicXElement(XElement source)

{

xmlSource = source;

}

public override bool TryGetMember(GetMemberBinder binder,out object result)

{

result = new DynamicXElement(null);

if (binder.Name == "Value")

{

result = (xmlSource != null) ? xmlSource.Value : "";

return true;

}

if (xmlSource != null)

result = new DynamicXElement(xmlSource.Element(XName.Get(binder.Name)));

//上面这句动态获取了XML节点属性对象,并又再次调用DynamicXElement类型构建递归查找对象

return true;

}

public override bool TryGetIndex(GetIndexBinder binder, object[] indexes, out object result)

{

result = null;

// This only supports [string, int] indexers

if (indexes.Length != 2)

return false;

if (!(indexes[0] is string))

return false;

if (!(indexes[1] is int))

return false;

var allNodes = xmlSource.Elements(indexes[0].ToString());

int index = (int)indexes[1];

if (index < allNodes.Count())

result = new DynamicXElement(allNodes.ElementAt(index));

else

result = new DynamicXElement(null);

return true;

}

public override string ToString()

{

if (xmlSource != null)

return xmlSource.ToString();

else

return string.Empty;

}

}

 

//使用该XML动态处理类型操作XML数据

// Create an XElement document containing

// solar system data:

var xml = createXML();

Console.WriteLine(xml);

dynamic dynamicXML = new DynamicXElement(xml);

// old way:

var firstPlanet = xml.Element("Planet");

Console.WriteLine(firstPlanet);

// new way:

// returns the first planet.

dynamic test2 = dynamicXML.Planet;

// gets the third planet (Earth)

dynamic test3 = dynamicXML["Planet", 2];

dynamic earthMoon = dynamicXML["Planet", 2]["Moons", 0].Moon;

dynamic test6 = dynamicXML["Planet", 2]["Moons", 3].Moon; // earth doesn't have 4 moons

dynamic fail = dynamicXML.NotAppearingInThisFile;

dynamic fail2 = dynamicXML.Not.Appearing.In.This.File;

 

  1. 实现System.Dynamic.IDynamicMetaObjectProvider接口

 

由于dynamic类型在运行时是动态创建对象的,不会有编译时的静态类型,所以对该类型的每个成员的访问都会调用GetMetaObject方法,以获取动态对象,然后通过该动态对象进行调用,不论你是第几次调用,该方法是否是静态方法。所以实现IDynamicMetaObjectProvider接口,你需要实现一个GetMetaObject方法来返回DynamicMetaObject对象。如下代码所示:

 

class DynamicDictionary2 : IDynamicMetaObjectProvider

    {

        #region IDynamicMetaObjectProvider Members

        DynamicMetaObject IDynamicMetaObjectProvider.GetMetaObject(System.Linq.Expressions.Expression parameter)

        {

            return new DynamicDictionaryMetaObject(parameter, this);

        }

        #endregion

     //这里定义了一个嵌套类DynamicDictionaryMetaObject ,其继承自DynamicMetaObject类型,并通过重载该类型的BindGetMember、BindSetMember、BindInvokeMember方法实现了对DynamicDictionary2 的GetDictionaryEntry及SetDictionaryEntry方法的动态访问

        class DynamicDictionaryMetaObject : DynamicMetaObject

        {

            internal DynamicDictionaryMetaObject(Expression parameter, DynamicDictionary2 dynamicDictionary)

                : base(parameter, BindingRestrictions.Empty, dynamicDictionary)

            {

 

            }

 

            public override DynamicMetaObject BindSetMember(SetMemberBinder binder, DynamicMetaObject value)

            {

                // Method to call in the containing class:

                string methodName = "SetDictionaryEntry";

                // setup the binding restrictions.

                BindingRestrictions restrictions = BindingRestrictions.GetTypeRestriction(Expression, LimitType);

                // setup the parameters:

                Expression[] args = new Expression[2];

                // First parameter is the name of the property to Set

                args[0] = Expression.Constant(binder.Name);

                // Second parameter is the value

                args[1] = Expression.Convert(value.Expression, typeof(object));

                // Setup the 'this' reference

                Expression self = Expression.Convert(Expression, LimitType);

                // Setup the method call expression

                Expression methodCall = Expression.Call(self, typeof(DynamicDictionary2).GetMethod(methodName), args);

                // Create a meta object to invoke Set later:

                DynamicMetaObject setDictionaryEntry = new DynamicMetaObject(methodCall, restrictions);

                // return that dynamic object

                return setDictionaryEntry;

            }

 

            public override DynamicMetaObject BindGetMember(GetMemberBinder binder)

            {

                // Method call in the containing class:

                string methodName = "GetDictionaryEntry";

                // One parameter

                Expression[] parameters = new Expression[]

            {

                Expression.Constant(binder.Name)

            };

                DynamicMetaObject getDictionaryEntry = new DynamicMetaObject(

                                    Expression.Call(

                                                Expression.Convert(Expression, LimitType),

                                                typeof(DynamicDictionary2).GetMethod(methodName),

                                                parameters),

                                    BindingRestrictions.GetTypeRestriction(Expression, LimitType));

                return getDictionaryEntry;

            }

 

            public override DynamicMetaObject BindInvokeMember(InvokeMemberBinder binder, DynamicMetaObject[] args)

            {

                StringBuilder paramInfo = new StringBuilder();

                paramInfo.AppendFormat("Calling {0}(", binder.Name);

                foreach (var item in args)

                    paramInfo.AppendFormat("{0}, ", item.Value);

                paramInfo.Append(")");

 

                Expression[] parameters = new Expression[]         

        {

            Expression.Constant(paramInfo.ToString())          

        };

                DynamicMetaObject methodInfo = new DynamicMetaObject(

                    Expression.Call(

                    Expression.Convert(Expression, LimitType),

                    typeof(DynamicDictionary2).GetMethod("WriteMethodInfo"),

                    parameters),

                    BindingRestrictions.GetTypeRestriction(Expression, LimitType));

                return methodInfo;

            }

 

        }

 

        private Dictionary<string, object> storage = new Dictionary<string, object>();

        public object SetDictionaryEntry(string key, object value)

        {

            if (storage.ContainsKey(key))

                storage[key] = value;

            else

                storage.Add(key, value);

            return value;

        }

        public object GetDictionaryEntry(string key)

        {

            object result = null;

            if (storage.ContainsKey(key))

            {

                result = storage[key];

            }

            return result;

        }

        public override string ToString()

        {

            StringWriter message = new StringWriter();

            foreach (var item in storage)

                message.WriteLine("{0}:\t{1}", item.Key,

                item.Value);

            return message.ToString();

        }

 

        public object WriteMethodInfo(string methodInfo)

        {

            Console.WriteLine(methodInfo);

 

            return 42; // because it is the answer to everything

        }

 

    }

 

注意:SetDictionary方法将赋值语句的右侧作为返回值返回,以保证类似如下的等式传递可用:

DateTime current = propertyBag2.Date = DateTime.Now;

 

注意:在使用赋值操作时,需要对赋值的值进行校验,以保证其是一个合法的赋值,像如下的语句的调用就是你为什么需要Restriction的原因。

propertyBag2.MagicNumber = GetMagicNumber();

 

另外,用反射获取dynamic对象的属性是不成立的,例如如下代码所示:

dynamic expando = new ExpandoObject();

expando.SampleProperty = "This property was added at run time";

PropertyInfo dynamicProperty = expando.GetType().GetProperty("SampleProperty");

// dynamicProperty is null.

 

由于dynamic声明的对象未必都实现了IDynamicMetaObjectProvider接口,因此,如果将动态功能与反射一起使用,则请记住,反射不适用于动态添加的属性和方法,并且最好检查正在反射的对象是否实现了 IDynamicMetaObjectProvider 接口。 如下代码所示:

dynamic expando = new ExpandoObject();

Console.WriteLine(expando is IDynamicMetaObjectProvider);

// True

dynamic test = "test";

Console.WriteLine(test is IDynamicMetaObjectProvider);

// False

 

对与数据驱动的业务逻辑,如在处理JSON或XML格式的数据的时候,你可以少创建些对应这些数据的静态类,而使用动态类型来处理这些数据,而且可以得到更好的异常处理方式,不会为数据的不完整添加更多的验证逻辑。Phil Haack在它的Fun With Method Missing and C# 4中描述了他如何在C#4.0中实现了method_missing (from Ruby),有空可以看下,受受启发。