许多类型都定义了能被获取或更改的状态信息。这个状态信息一般作为类型的字段成员实现。
如下代码,我们为这个类型定义两个字段:
创建该类型的实例后,可以使用以下形式的代码轻松获取或者设置它的状态信息:
这种查询和设置对象状态信息的做法十分常见。但是,永远不应该像这样实现。
面向对象设计和编程的重要原则之一就是数据封装,意味着类型的字段永远不应该公开,否则很容易因为不恰当使用字段而破坏对象的状态。
如下代码,我们可以很容易地破坏一个Employee对象:
//--
还有其他原因促使我们封装类型中的数据字段的访问。
1.你可能希望访问字段来执行一些副作用(即side effect;在计算机编程中,如果一个函数或表达式除了生成一个值,还会造成状态的改变,就说它会造成副作用;或者说会执行一个副作用)、缓存某些值或者推迟创建一些内部对象(推迟创建对象是指在对象第一次需要时才真正创建它)。
2.你可能希望以线程安全的方式访问字段。
3.字段可能是一个逻辑字段,它的值不由内存中的字节表示,而是通过某个算法来计算获得。
//--
基于上述原因,强烈建议将所有字段都设为private。要允许用户或类型获取或设置状态信息,就公开一个针对该用途的方法。封装字段访问的方法通常称为访问器(accessor)方法。
访问器方法可选择对数据的合理性进行检查,确保对象的状态永远不被破坏。
虽然这是个简单地例子,但是还可以看出数据字段封装带来的巨大好处。另外可以看出,实现只读属性或只写属性是多么简单,只需选择不实现一个访问器方法即可。另外,将SetXXX方法标记为protected,就可以只允许派生类型修改值。
但是,像这样进行数据封装有两个缺点:
1.因为不得不实现额外的方法,所以必须写更多的代码;
2.类型的用户必须调用方法,而不能直接引用字段名。
//--
编程语言和CLR还是提供了一个称为属性(property)的机制。它缓解了第一个缺点所造成的影响,同时完全消除了第二个缺点。
可将属性想象成智能字段,即背后有额外的字段。CLR支持静态、实例、抽象和虚属性。另外,属性可用任意“可访问性”修饰符来标记,而且可以在接口中定义。
每个属性都有名称和类型(类型不能是void)。属性不能重载,即不能定义名称相同、类型不同的两个属性。定义属性时指定get和set两个方法,但可省略set方法来定义只读属性,或省略get方法来定义只写属性。
经常利用属性的get和set方法操纵类型中定义的私有字段。私有字段通常称为支持字段(backing filed)。但get和set方法并非一定要访问支持字段。
定义属性时,取决于属性的定义,编译器在最后的托管程序集中生成一下两项或三项。
1.代表属性get访问器的方法。仅在属性定义了get访问器方法时生成。
2.代表属性set访问器的方法。仅在属性定义了set访问器方法时生成。
3.托管程序集元数据中的属性定义。这一项必然生成。
编译器在你指定的属性之前自动附加get_或set_前缀类生成方法名。C#内建了对属性的支持。C#编译器发现代码试图获取或设置属性时,实际会生成对上述某个方法的调用。即使编程语言不直接支持属性,也可调用需要的访问器方法来访问属性。效果一样,只是代码看起来没那么优雅。
除了生成访问器方法,针对源代码中定义的每一个属性,编译器还会在托管程序集的元数据中生成一个属性定义项。在这个记录项中包含一些标志(flags)以及属性的类型。另外,它还引用了get和set访问器方法。这些信息唯一的作用就是在“属性”这种抽象概念与它的访问器方法之间建立起一个联系。编译器和其他工具可利用这种元数据信息(使用System.Reflection.PropertyInfo类来获取)。CLR不适用这种元数据信息,在运行时只需要访问器方法。