天天看点

改善C#编程的50个建议(41-45)

41 为数据驱动的动态类型使用DynamicObject或IDynamicMetaObjectProvider

  动态编程的一个优势是当公共接口在运行时改变了,它会基于你怎么使用而建立新的类型。C#提供了System.Dynamic.DynamicObject基类和System.Dynamic.IDynamicMetaObjectProvider接口

工具。使用这些工具你能创建你自己的带动态能力的类型.

  最简单的创建具有动态能力类型的方法是继承System.Dynamic.DynamicObject。这个类型使用私有嵌套类实现了IDynamicMetaObjectProvider接口.这个私有嵌套类做了大部分的困难工作,包括

解析表达式和转发这些到DynamicObject类的对应虚拟方法。这使得其使用起来比较简单。

  例如:假设有一个实现了动态属性包的类,当你第一次创建DynamicPropertyBag,它不会有任何项,因此它没有任何属性。当你尝试检索任何属性时,它将抛出异常。你可以通过调用setter增加

任何属性到包里.在增加属性后,你可以调用getter来访问任何属性。

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";

 实现动态属性需要重写DynamicObject基类的TrySetMember和TryGetMember方法.

    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();

    }

这个动态属性包包含了一个存储属性名和值的字典。

TryGetMember检查请求名,并且如果属性已经存储在字典,TryGetMember将返回它的值,如果值没有存储,那么动态调用将失败。

TrySetMember和TryGetMember类似,它检查请求名,并且在内部字典更新或创建一个条目.因为你能创建任何属性,所以TrySetMember

方法总是返回TRUE,指示这个动态调用成功了。

  DynamicObject包含了相似的方法来处理索引器、方法、构造函数、一元和二元操作的动态调用.你可以重写它们来创建你自己的动态成员.

  使用XML中,LINQ到XML做了一些重大改进。但是它仍有些地方不如人意。参考下面的xml代码片段:

<Planets>

  <Planet>

  <Name>Mercury</Name>

  </Planet>

  <Name>Venus</Name>

  <Name>Earth</Name>

  <Moons>

  <Moon>Moon</Moon>

  </Moons>

  <Name>Mars</Name>

  <Moon>Phobos</Moon>

  <Moon>Deimos</Moon>

  <!-- other data elided -->

</Planets>  

为了得到第一个Planet,你可能像这样写:

var xml = createXML();

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

这不是太坏,但你进入的越深,编写的代码将越复杂。获得Earth(第3个Planet)的代码可能是这样了:

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

获取第3个Planet的名字需要更多代码:

var earthName = xml.Elements("Planet").Skip(2).First().Element("Name");

一旦你需要获取Moon,它就变的很长了:

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

  更进一步的,上面的代码是假定XML包含你寻找到节点。如果XML文件有问题,其中某些节点缺失的话,上面的代码会抛出一个异常。

增加处理缺失节点的代码将会增加更多的代码,而这仅仅是为了处理潜在的错误。那个时候将很难识别原来代码的意图了。

  相反,假设你有一个数据驱动类型,它能给你XML元素的点记法.此时查找第一个planet将会简化为:

// Create an XElement document containing

// solar system data:

Console.WriteLine(xml);

dynamic dynamicXML = new DynamicXElement(xml);

// 旧的方式:

Console.WriteLine(firstPlanet);

// 新的方式:

// returns the first planet.

dynamic test2 = dynamicXML.Planet;

获得第3个planet将会更简单(使用索引器):

// gets the third planet (Earth)

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

获取Moon:

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

最后,由于是动态的,你可以定义任何缺失节点返回空元素:

dynamic test6 = dynamicXML["Planet", 2]["Moons", 3].Moon; //返回空元素

dynamic fail = dynamicXML.NotAppearingInThisFile; //返回空元素

dynamic fail2 = dynamicXML.Not.Appearing.In.This.File; //返回空元素

因为缺失元素将会返回一个缺失动态元素,你可以继续解引用它,并且当组成XML导航的任何元素缺失时,最终结果将会是一个缺失的元素。

建立这些需要从DynamicObject派生出另一个类来实现。你需要重写TryGetMember、TryGetIndex来返回对应节点的动态元素。

    public class DynamicXElement : DynamicObject

        private readonly XElement xmlSource;

        public DynamicXElement(XElement source)

            xmlSource = source;

            result = new DynamicXElement(null);

            if (binder.Name == "Value")

                result = (xmlSource != null) ?

                xmlSource.Value : "";

            if (xmlSource != null)

                result = new DynamicXElement(

                xmlSource.Element(XName.Get(binder.Name)));

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

            // This only supports [string, int] indexers

            if (indexes.Length != 2)

                return false;

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

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

            var allNodes = xmlSource.Elements(indexes[0].

            ToString());

            int index = (int)indexes[1];

            if (index < allNodes.Count())

                result = new DynamicXElement(allNodes.ElementAt(

                index));

                result = new DynamicXElement(null);

                return xmlSource.ToString();

                return string.Empty;

TryGetIndex方法是新方法。当客户端代码调用索引器来检索一个元素时,它必须实现动态行为.    

  使用DynamicObject使得更容易实现一个行为动态的类型。它隐藏了创建动态类型的大部分复杂性.但有时你想创建一个动态类型而又不想使用DynamicObject做基类.

基于这个原因,我们准备展示如何通过实现IDynamicMetaObjectProvider接口来创建动态字典。

  实现IDynamicMetaObjectProvider意味着实现GetMetaObject方法.这里有个实现了IDynamicMetaObjectProvider的DynamicDictionary版本,它不用继承DynamicObject.

    class DynamicDictionary2 : IDynamicMetaObjectProvider

        #region IDynamicMetaObjectProvider Members

        DynamicMetaObject IDynamicMetaObjectProvider.

        GetMetaObject(System.Linq.Expressions.Expression parameter)

            return new DynamicDictionaryMetaObject(parameter,this);

        #endregion

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

        public object SetDictionaryEntry(string key,object value)

            return value;

        public object GetDictionaryEntry(string key)

            object result = null;

                result = storage[key];

            return result;

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

每当它被调用时,GetMetaObject()都返回一个新的DynamicDictionaryMetaObject.如果你调用相同的成员10次,GetMetaObject()将被调用10次。即使方法

被定义为静态方法,GetMetaObject()将仍被调用并且能拦截这些可能调用动态行为的方法。记住静态类型的动态对象也是动态的,因此没有编译时的行为被定义。

每个成员的访问都是被动态分派的。

  DynamicMetaObject负责建立执行任何必须处理动态调用的代码的表达式树.它的构造函数以表达式和动态对象做参数。在被构造后,绑定方法中的一个将

会被调用。这个绑定方法负责构造一个包含执行动态调用表达式的DynamicMetaObject.DynamicDictionary中必须实现的两个绑定方法为:BindSetMember和

BindGetMember.

  BindSetMember构造一个表达式树,它将调用DynamicDictionary2.SetDictionaryEntry()设置一个字典的值.下面是它的实现:

        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;

元编程很容易使人混乱,让我们慢慢分析。第一行设置了DynamicDictionary中被调用的方法名--SetDictionaryEntry。注意

SetDictionary返回属性赋值操作的右操作数.这是很重要的,否则下面的代码将不会工作:

DateTime current = propertyBag2.Date = DateTime.Now;

  然后这个方法初始化一个BindingRestrictions集合.大部分时候,你将会像这里一样使用约束,给定源表达式的约束并作为动态

调用的目标类型.

  剩下的构造了调用表达式的方法,它将使用属性名和值调用SetDictionaryEntry().属性名是一个常量表达式,但值是一个转换表达式.

记住setter的右边可以是一个方法或者是带副作用的表达式。在适当时候,这些必须被评估。否则,使用方法返回值做属性的值将不会工作:

propertyBag2.MagicNumber = GetMagicNumber();

当然,为了实现字典,你也必须实现BindGetMember.BindGetMember几乎是和BindSetMember一样的工作方式.它构造一个表达式来检索字典中

属性的值。

       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;

表达式树很难调试。它们很难写正确。越复杂的动态类型代码也就越多,这意味着表达式也就越难写正确。

  此外,记住我开始说的那句:在你的动态对象中的每个调用将创建一个新的DynamicMetaObject,并且调用其中一个绑定成员.

在写这些方法的时候,你将需要留意效率和性能.这些方法被频繁调用,而且它们需要做很多工作。

  实现动态行为作为一种很好的方式来适应你的一些编程挑战。当你看到创建动态类型,你的第一选择应当是继承System.Dynamic.DynamicObject.

在你必须使用一个不同基类的时候,你能实现你自己的IDynamicMetaObjectProvider,但是记住这是一个复杂的问题。进一步说,任何动态类型调用

将产生一些性能开销,你自己实现它可能会产生更多开销.

42 理解如何使用表达式API

  .NET有一些API可以使你基于类型做反射或在运行时创建代码.这种在运行时检查或创建代码的能力是非常有用的.很多问题都能被代码检查或动态生成

代码很好地解决。作为开发者,我们渴望一种更简单的方式来动态地解决问题.

  现在C#支持LINQ和动态,你可以有更好的方式而不再是使用传统的反射API,这种方式就是:表达式和表达式树.表达式看起来像代码,而且在许多时候,表达式

被编译为委托。尽管如此,你可以以一个表达式格式来要求表达式.你可以检查那个表达式,就像你可以使用反射API检查一个类一样。另一方面,你可以建立

一个表达式来创建运行时的代码。一旦你创建了表达式树,你就可以编译和执行表达式。它有无限的可能性.在这之后,你就可以在运行时创建代码了。我将描述

两个常见的问题,当使用表达式的时候可以使它更容易.

  第一个问题是在通信框架里.通常的工作流是使用WCF,远程调用或者Web Service技术来使用一些代码生成工具为特定服务生成一个客户端代理。它工作了,但

这几乎是一个重量级的解决方案.你将生成数百行代码。一旦服务端有了一个新方法或者改变了参数列表,你将需要更新这个代理。相反地,假设你写如下代码:

var client = new ClientProxy<IService>();

var result = client.CallInterface<string>(srver => srver.DoWork(172));

这里,ClientProxy<T>知道如何将每个参数和方法在网络上调用。尽管这样,它不知道实际访问的服务的任何东西.不是依赖一些便利代码生成器,它将使用表达式树

和泛型来解决哪些方法你调用了,哪些参数你使用了。

  CallInterface()方法有一个参数,它是Expression<Func<T, TResult>>类型.输入参数(T类型)代表一个实现了IService的对象.TResult是这些特定方法的返回.

参数是一个表达式,你甚至不需要实现一个IService的实例对象来写这个代码。核心算法在CallInterface()方法内.

      public TResult CallInterface<TResult>(Expression<Func<T, TResult>> op)

            var exp = op.Body as MethodCallExpression;

            var methodName = exp.Method.Name;

            var methodInfo = exp.Method;

            var allParameters = from element in exp.Arguments

                                select processArgument(element);

            Console.WriteLine("Calling {0}", methodName);

            foreach (var parm in allParameters)

                Console.WriteLine("\tParameter type = {0}, Value = {1}",

                parm.Item1, parm.Item2);

            return default(TResult);

        private Tuple<Type, object> processArgument(Expression element)

            object argument = default(object);

            LambdaExpression l = Expression.Lambda(

            Expression.Convert(element, element.Type));

            Type parmType = l.ReturnType;

            argument = l.Compile().DynamicInvoke();

            return Tuple.Create(parmType, argument);

  从CallInterface开始,首先查看表达式树的主体。那就是lambda操作的右边部分.(srver.DoWork(172)) 它是一个MethodCallExpression,

并且MethodCallExpression包含了所有你需要理解的所有参数信息和调用的方法名.方法名非常简单:它存在在Method属性的Name属性中.

在这个例子中,方法名就是‘DoWork‘.LINQ查询处理这个方法的任何参数. 这个有趣的处理工作在这里是processArgument来完成.  

  processArgument评估每个参数表达式。在上面的例子中,只有一个参数,而且是一个常数172.尽管如此,这不是非常健壮的,所以这个代码

使用了不同的策略.它不健壮,因为参数可以是调用的方法,属性或者索引访问器,或者字段访问器.任何一个方法调用都能包含这些类型参数.

这个方法利用LambdaExpression类型和评估每个参数表达式来处理这个困难的工作而不是尝试去解析它们。每个参数表达式,甚至是ConstantExpression,

都能以一个lambda表达式的返回值来表示.ProcessArgument()转换参数到LambdaExpression.在这里,它的转换操作等价于()=>172.这个方法

转换每个参数到一个lambda expression,因为lambda expression能被编译为一个可被调用的委托. 在这里,它创建一个返回常量172的委托.

越复杂的表达式将创建越复杂的lambda expressions.

  一旦lambda expression被创建,你可以从lambda中检索参数类型.注意这个方法不对参数做任何处理。当lambda expression调用时,lambda expression

中的参数评估将被执行.这个方法可以嵌套调用,如:

client.CallInterface(srver => srver.DoWork(client.CallInterface(srv => srv.GetANumber())));  

这个技术展示了如何使用表达式树来决定用户希望在运行时执行的代码.它很难在书中展示出来,但因为ClientProxy<T>是一个使用服务接口做参数的泛型类.

CallInterface方法又是强类型.lambda expression中的方法必须以定义在服务上的一个成员方法来调用。        

  第一个例子给你展示了如何解析表达式来转换代码到你需要实现的运行时算法.第二个例子展示了相反的方面:有时你需要在运行时生成代码.在大型系统中

一个常见的问题是需要根据一些相关的源类型来创建一些目标类型的对象.例如,你的大型企业可能包含来自定义不同联系类型的供应商的系统,你最好创建一些

类型来显示实现,而不是用方法来解决.

var converter = new Converter<SourceContact,DestinationContact>();

DestinationContact dest2 = converter.ConvertFrom(source);

你将期望converter从源到目标复制每一个有相同名字的属性(源对象需要有一个公共的get访问器,目标对象有一个公共的set访问器).这种运行时代码生成可以通

过表达式来很好地处理,并能很好地被编译和执行.你希望生成的代码做像下面一样的事情:

//不是C#语法,仅做解释而已

TDest ConvertFromImaginary(TSource source)

  TDest destination = new TDest();

  foreach (var prop in sharedProperties)

  destination.prop = source.prop;

  return destination;

你需要创建一个表达式来创建执行上面伪代码的代码。这里有一个完整的方法来创建表达式并编译到一个函数,随后我将详细解释该方法的每个部分.

        private void createConverterIfNeeded()

            if (converter == null)

                var source = Expression.Parameter(typeof(TSource),"source");

                var dest = Expression.Variable(typeof(TDest),"dest");

                var assignments = from srcProp in

                                      typeof(TSource).GetProperties(

                                      BindingFlags.Public |BindingFlags.Instance)

                                  where srcProp.CanRead

                                  let destProp = typeof(TDest).

                                  GetProperty(

                                  srcProp.Name,

                                  BindingFlags.Public |

                                  BindingFlags.Instance)

                                  where (destProp != null) &&

                                  (destProp.CanWrite)

                                  select Expression.Assign(

                                  Expression.Property(dest,

                                  destProp),

                                  Expression.Property(source,

                                  srcProp));

                // put together the body:

                var body = new List<Expression>();

                body.Add(Expression.Assign(dest,Expression.New(typeof(TDest))));

                body.AddRange(assignments);

                body.Add(dest);

                var expr =Expression.Lambda<Func<TSource, TDest>>(

                Expression.Block(

                new[] { dest }, // expression parameters

                body.ToArray() // body

                ),

                source // lambda expression

                );

                var func = expr.Compile();

                converter = func;

这个方法模拟了上面的伪代码。首先你声明了参数:

var source = Expression.Parameter(typeof(TSource), "source");

然后,你需要声明一个局部变量来保存目标:

var dest = Expression.Variable(typeof(TDest), "dest");

随后大部分的代码都是从源对象到目标对象的属性赋值.我使用了LINQ查询来写这个代码.LINQ查询的源是源对象的所有公共实例属性:

from srcProp in typeof(TSource).GetProperties(

BindingFlags.Public | BindingFlags.Instance)

where srcProp.CanRead

let声明了一个局部变量来保存与目标类型相同名称的属性.如果目标类型没有对应类型的属性,它会是null:

let destProp = typeof(TDest).GetProperty(srcProp.Name,

where (destProp != null) &&(destProp.CanWrite)

接下来的LINQ部分是一系列赋值语句,它将从源对象中找到与目标对象有相同属性名的值赋值给目标对象的属性.

select Expression.Assign(

Expression.Property(dest, destProp),

Expression.Property(source, srcProp));

方法剩下的部分是建立lambda表达式的主体.表达式类的Block()方法需要一个表达式数组的所有语句.下一步就创建一个List<Expression>,这样你可以

增加所有语句到数组.列表很容易转换到数组:

var body = new List<Expression>();

body.Add(Expression.Assign(dest,Expression.New(typeof(TDest))));

body.AddRange(assignments);

body.Add(dest);

最后,是时候创建返回目标对象和包含所有建立的语句的lambda了:

var expr =

Expression.Lambda<Func<TSource, TDest>>(

Expression.Block(

new[] { dest }, // expression parameters

body.ToArray() // body

),

source // lambda expression

);

然后编译它并将它转换为一个可以调用的委托:

var func = expr.Compile();

converter = func;

这是比较复杂的,也不容易书写。你可能会经常碰到运行时的编译错误直到表达式正确.对于简单问题,这当然也不是最好的处理方式.但即便如此,

表达式API仍比反射API要简单。当你想使用反射的时候,尝试使用表达式API来解决问题吧.

  表达式API可以使用在两种不同的方式:你可以创建一个使用表达式做参数的方法,它会使你去解析这些表达式并基于被调用的表达式背后的概念

创建代码.另外,表达式API可以使你在运行时创建代码。你可以创建类的代码,然后执行它.这非常有用,它可以解决一些你遇到的更复杂的通用问题.

43 使用表达式将后期绑定转换为前期绑定

  后期绑定API使用符号文本来完成它们的工作.而已经编译的API不需要那些信息,因为编译器已经解决了符号引用.表达式API构造了这两者的桥梁。

表达式对象包含一种抽象符号树的形式来表示你想执行的算法.你可以使用表达式API来执行那些代码。你也可以检查所有的符号,包含变量名,方法和

属性。你可以使用表达式API来创建强类型的编译方法,它可以与依赖于后期绑定的系统部分交互,并且使用了属性名或其他符号.

  后期绑定API的一个最常见例子是Silverlight和WPF使用的通知接口属性.Silverlight和WPF设计用来响应绑定属性的改变,因此当用户界面下的数据元素

改变时用户接口元素也能响应.当然这不是魔术,只需要你去实现一些代码。在这个例子上,你需要实现两个接口:INotifyPropertyChanged和INotifyPropertyChanging.

它们都是非常简单的接口,每个支持一个事件.两个事件的参数简单包含要被更新的属性名.你可以使用表达式API来移除属性名的依赖.

  后期绑定实现这些属性是非常简单的.你的数据类需要声明支持那两个接口.每个能被改变的属性需要额外的代码来抛出事件.这里有个类显示了当前程序

使用的内存数量。它每3秒更新一次.通过支持INotifyPropertyChanged和INotifyPropertyChanging接口,这个类型的对象可以被增加到你的窗口类,你也可以

看到你的运行时内存使用情况.

public class MemoryMonitor : INotifyPropertyChanged,INotifyPropertyChanging

    System.Threading.Timer updater;

    public MemoryMonitor()

        updater = new System.Threading.Timer((_) => timerCallback(_), null, 0, 500);

    private void timerCallback(object unused)

        UsedMemory = GC.GetTotalMemory(false);

    public long UsedMemory

        get { return mem; }

        private set

            if (value != mem)

                if (PropertyChanging != null)

                    PropertyChanging(this,

                    new PropertyChangingEventArgs(

                    "UsedMemory"));

                mem = value;

                if (PropertyChanged != null)

                    PropertyChanged(this,

                    new PropertyChangedEventArgs(

    private long mem;

    #region INotifyPropertyChanged Members

    public event PropertyChangedEventHandler

    PropertyChanged;

    #endregion

    #region INotifyPropertyChanging Members

    public event PropertyChangingEventHandler

    PropertyChanging;

这就是所有的代码.但是,每次你创建它们中的任何一个接口的实现,你会问自己还有没有更简单的方式来实现.每个属性setter需要抛出一个事件.这

并不是一个好方法.但是你看得出来每个setter需要抛出两个事件:一个在属性改变前,一个在属性改变后.事件参数使用一个字符串来表示属性名字,这

是不好的,很脆弱。任何重构将会打破代码.任何输入错误将创建坏的代码.让我们来修复它.

  显而易见的选择是在一个扩展方法里实现东西.像许多事情一样,看起来容易做起来难.但是这个困难的代码仅需要写一次,所以值得我们去写,一般流

程如下:

  1.看新值和旧值是否不同.

  2.抛出INotifyPropertyChanging事件.

  3.改变属性值

  4.抛出INotifyPropertyChanged事件

困难的部分是使用什么字符串来做属性名.

  我最初的目标是使用扩展方法来扩展INotifyPropertyChanged或INotifyPropertyChanging,但那会使API更糟,主要原因是它使抛出事件更困难.相反,

方法实际扩展了INotifyPropertyChanged的PropertyChanged事件.下面展示如何在MemoryMonitor中使用它:

        long updatedValue = GC.GetTotalMemory(false);

        PropertyChanged.SetNotifyProperty(updatedValue,() => UsedMemory);

        get;

        private set;

 这使得MemoryMonitor的实现更容易了。UsedMemory现在是一个自动属性.比较复杂点的实现是使用了反射和表达式树.下面是整个扩展方法代码:

 public static class PropertyNotifyExtensions

    public static T SetNotifyProperty<T>(thisPropertyChangedEventHandler handler,

    T newValue, Expression<Func<T>> oldValueExpression,

    Action<T> setter)

        return SetNotifyProperty(handler, null, newValue,

        oldValueExpression, setter);

    public static T SetNotifyProperty<T>(this PropertyChangedEventHandler postHandler,

    PropertyChangingEventHandler preHandler,

        Func<T> getter = oldValueExpression.Compile();

        T oldValue = getter();

        if (!oldValue.Equals(newValue))

            var body = oldValueExpression.Body as

            System.Linq.Expressions.MemberExpression;

            var propInfo = body.Member as PropertyInfo;

            string propName = body.Member.Name;

            // Get the target object

            var targetExpression = body.Expression as

            ConstantExpression;

            object target = targetExpression.Value;

            if (preHandler != null)

                preHandler(target, new

                PropertyChangingEventArgs(propName));

            // Use Reflection to do the set:

            // propInfo.SetValue(target, newValue, null);

            //var compiledSetter = setter.Compile();

            setter(newValue);

            if (postHandler != null)

                postHandler(target, new

                PropertyChangedEventArgs(propName));

        return newValue;

在讲解代码之前,让我开始一个简单的声明.我移除了一些错误处理的代码,在生产代码上,你需要检查转换是否正常工作.你也需要处理在Silverlight

沙箱中可能的安全异常.

  第一个动作就是编译和执行get属性表达式,并将其值和新值做对比.如果新旧值一样就不用做任何工作.

  下面的部分更复杂了.这些代码解析表达式来发现需要的设置值和抛出INotifyPropertyChanging和INotifyPropertyChanged事件的重要组件.意思就是

找到属性名、目标对象的类型,并访问setter属性.记住这个方法是如何被调用的.通过表达式映射oldValueExpression:() => UsedMemory.这是一个成员

访问表达式,成员访问表达式包含成员属性(被改变的属性的PropertyInfo).它其中的一个成员是属性名,也就是你获得字符串"UsedMemory"的地方,也是你

将要抛出事件的地方.PropertyInfo对象还有另外的用处,你可以对它使用反射API来改变属性的值.

  实际上LINQ To SQL和实体框架都是建立在System.Linq.Expression API上.这些API允许你把代码当数据一样.你可以使用表达式API来检查代码.你可以

改变算法,创建新代码,执行代码.这是构建动态系统的一个好方法.

  数据绑定,就其本质而言,需要你使用字符串来表示你的属性.INotifyPropertyChanged和INotifyPropertyChanging没有异常.这是一个重要的特性,你应

该喜欢在任何可能有数据绑定对象的类中支持这些接口.它也使得你足够创建一个通用的解决方案.

44 在公共API中最小化动态对象

  动态对象不一定会在静态类型系统中表现的很好.静态类型系统会把它视作System.Object的实例.但是它们是特殊的实例.

  但是动态对象太强势了,任何它们触碰到的都变成了动态.执行操作的任何一个参数是动态时,结果也肯定是动态了.从方法返回一个动态对象,任何使用这个

动态对象的地方也会变成动态.就像培养皿中的细菌一样.不久,一切都会是动态.生物学家在培养皿中生长细胞,也在哪里限制它们生长.动态也一样,在一个隔离

的环境中使用动态对象并且有时返回静态对象而不是动态对象.否则,动态会带来不良影响,应用中所有调用将会变成动态.

  这并不是说动态都不好.这章的其他项给你展示了动态编程技术是一个很好的解决方法.但是动态类型和静态类型是完全不同的.混合这两者需要注意许多错误

和副作用.C#是一种静态类型语言,在某些地方启用了动态类型.因此,如果你使用C#,你应把大部分时间花费在使用静态类型上并最小化动态特性的使用范围.如果你

想彻底的使用动态编程,你应考虑使用一种动态语言而不是静态类型的语言.

  如果你准备在程序中使用动态特性,尽量保持它们在对外的公共接口.一个使用动态类型的场景是与在动态环境中创建的对象交互,如IronPython.当你设计使用动态

语言来创建动态对象时,你应将它们封装在C#对象里,这样其余的C#可以不知道有动态类型的发生.

  对问题38中的例子来说:

dynamic answer = Add(5, 5);

Console.WriteLine(answer);

编译器生成的代码不少,而且每次调用还要动态生成.你可以用一些泛型语法来封装Add()方法.

//原代码

private static dynamic DynamicAdd(dynamic left,dynamic right)

  return left + right;

//封装它

public static T1 Add<T1, T2>(T1 left, T2 right)

  dynamic result = DynamicAdd(left, right);

  return (T1)result;

这样编译器生成的所有动态调用代码都在这个泛型方法Add()内了.这样隔离动态到一个位置.通过这个例子,你看到动态类型可以被隔离到最小的范围.当代码需要

使用动态特性时,例子展示了局部变量result是动态的.这个访问将转换动态对象到一个强类型对象,这样动态对象将不会离开函数的范围.

  我们经常以不同的形式使用CSV数据.读取并解析CSV数据是一个比较简单的联系,但是总是缺少一个通用的解决方法.下面的代码读取两个不同的CSV文件并显示:

        var data = new CSVDataContainer(new System.IO.StringReader(myCSV));

        foreach (var item in data.Rows)

            Console.WriteLine("{0}, {1}, {2}",item.Name, item.PhoneNumber, item.Label);

        data = new CSVDataContainer(new System.IO.StringReader(myCSV2));

            Console.WriteLine("{0}, {1}, {2}",item.Date, item.high, item.low);

这是一个我想要的API风格的通用CSV读取类。行通过枚举包含数据的属性来返回.显然,行标题名在编译时并不知道.这些属性必然是动态的.但是CSVDataContainer

的其他都不需要动态,CSVDataContainer不支持动态类型.尽管如此,CSVDataContainer包含了返回动态对象的API:

    public class CSVDataContainer

        private class CSVRow : DynamicObject

            private List<Tuple<string, string>> values =new List<Tuple<string, string>>();

            public CSVRow(IEnumerable<string> headers,IEnumerable<string> items)

                values.AddRange(headers.Zip(items,

                (header, value) => Tuple.Create(header,

                value)));

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

                var answer = values.FirstOrDefault(n =>n.Item1 == binder.Name);

                result = answer.Item2;

                return result != null;

        private List<string> columnNames = new List<string>();

        private List<CSVRow> data = new List<CSVRow>();

        public CSVDataContainer(System.IO.TextReader stream)

            // read headers:

            var headers = stream.ReadLine();

            columnNames =

            (from header in headers.Split(‘,‘)

             select header.Trim()).ToList();

            var line = stream.ReadLine();

            while (line != null)

                var items = line.Split(‘,‘);

                data.Add(new CSVRow(columnNames, items));

                line = stream.ReadLine();

        public dynamic this[int index]

            get { return data[index]; }

        public IEnumerable<dynamic> Rows

            get { return data; }

 虽然你需要暴露一个动态类型来作为你接口的一部分,但它仅仅是动态机制的需要.这些API都是动态的,也必须是动态的.

你不可能支持所有可能的CSV格式而不需要动态来支持这些列名.你可以暴露使用动态的一切东西,但这个接口中的动态只出现在动态

功能需求的地方.

  动态类型是一个有用的特性,甚至在C#这样的静态类型语言中也是有用的.动态编程是有用的,但在C#中它最有用的地方是当你把它

局限于需要它的地方并且转换动态对象为不同的静态类型时.当你的代码依赖于另一个环境创建的动态类型时,封装这些动态对象并使用不同

的静态对象提供一个公共的接口.

45 减少装箱和拆箱

  值类型是数据的容器。它不是多态的。.NET框架设计有一个引用类型System.Object来作为所有对象的根。.NET框架使用装箱和拆箱来

作为这两者之间的桥梁。装箱将一个值类型放在一个未定义引用对象里,这样来使值类型当引用类型使用. 拆箱从已装箱的值类型中复制一份

值的副本.当你希望在使用System.Object类型的地方使用值类型的时候,拆箱和装箱是很必要的.但是装箱和拆箱也总是性能杀手.有时,当装箱

和拆箱创建临时对象的副本时,它还可能会导致一些细微的BUG出现.可能的话,应当避免拆箱和装箱.

  装箱将值类型转换为引用类型.装箱将在堆上分配一个新的引用对象,并且复制值类型存储的内容到引用对象里.下图演示了装箱对象是如何

被存储和访问的:

 图box

很多方面,.NET2.0里添加的泛型可以使你简单地避免装箱和拆箱.这也是最有效的避免不必要装箱操作的方法.尽管这样,在.NET框架总,有很多

方法还是使用System.Object做参数类型.这些API将仍然产生装箱和拆箱操作.它都是自动发生的.当你在引用类型的地方使用值类型的时候,编译

器自动生成装箱和拆箱指令.装箱和拆箱操作也可能发生在通过接口指针使用值类型时.甚至下面的简单语句也会发生装箱操作:

Console.WriteLine("A few numbers:{0}, {1}, {2}",25, 32, 50);

重载的Consol.WriteLine方法使用一组System.Object引用做参数.此时作为值类型的int必须被装箱,这样才能传递到这个重载的方法里去.在这个

方法里,代码会使用已装箱对象的ToString()方法.某种意义上,这和下面的代码一样:

int i = 25;

object o = i; // box

Console.WriteLine(o.ToString());  

你也可以在传递之前转换类型为string:

Console.WriteLine("A few numbers:{0}, {1}, {2}",25.ToString(), 32.ToString(), 50.ToString());

这代码使用了已知的整型类型.这个常见的例子演示了避免装箱的第一条原则:注意到System.Object的隐式转换.如果你能避免它,值类型不应被System.Object代替.

  另一个常见的例子是当你将值类型放在1.x框架的集合里时,你可能不注意地使用System.Object代替了一个值类型.你应使用2.0版本中的泛型集合(BCL中的)

来代替1.x中基础集合对象.但一些BCL组件仍使用了1.x风格的集合,你应了解这些问题,然后知道如何去避免它.

  .NET框架集合的第一个典型是它存储了System.Object实例的引用.任何时候你增加一个值类型到集合里,它会装箱.拆箱总是会产生一个副本。这可能会产

生一些小BUG.并且编译器不能帮你找到这些BUG,这都是因为装箱导致.我们以一个简单的结构体开始,并让你修改它的一个字段,然后将一些对象放到集合里:

    public struct Person

        public string Name { get; set; }

            return Name;

// 使用Person在集合中

var attendees = new List<Person>();

Person p = new Person { Name = "Old Name" };

attendees.Add(p);

// 尝试更改name

// Person是引用类型的话,将正常工作

Person p2 = attendees[0];

p2.Name = "New Name";

// 仍输出"Old Name":

Console.WriteLine(attendees[0].ToString( ));

这里Person是一个值类型.JIT编译器为List<Person>创建了一个特定的封闭的泛型类型,所以Person没有被装箱,它们都存储在attendees集合.当你移动Person对象来

访问Name属性并改变它时,另一个副本将产生.所有你做的改变都只是改变了副本.实际上,第三个副本也产生了,它产生在通过attendees[0]对象调用ToString()函数时.

基于这个原因,你应创建不可变的值类型.

  是的,值类型能被转换为System.Object或任何接口引用.这些转换隐式发生,这增加了发现它们的复杂性。装箱和拆箱操作产生的副本可能不是你所希望的.那会导致

一些BUG.这也会产生一个性能开销.留意任何转换值类型到System.Object或接口类型的结构:如将值类型放在集合里,调用带System.Object的方法,转换到System.Object等.

尽可能地避免这些.

继续阅读