原文:[CLR via C#]10. 属性
一、无参属性
对于字段,强烈建议将所有的字段都设为private。如果允许用户或类型获取或设置状态信息,就公开一个针对该用途的方法。封装了字段访问的方法通常称为访问器(accessor)方法。访问器方法可选择对数据的合理性进行检查,确保对象的状态永远不被破坏。如下代码:
private sealed class Employee { private String m_Name; private Int32 m_Age; public String GetName(){ return m_Name; } public void SetName(String value){ m_Name = value; } public Int32 GetAge(){ return m_Age; } public void SetAge(Int32 value){ if (value <= 0) throw new ArgumentOutOfRangeException("value", "must be >0"); m_Age = value; } }
想这样的数据封装有两个缺点。第一:不得不实现额外的方法;第二、用户必须调用方法。
CLR提供了属性(Property)的机制,它缓解了第一个缺点所造成的的影响,同事完全消除了第二个缺点。如下面代码:
private sealed class Employee { private String m_Name; private Int32 m_Age; public String Name { get { return (m_Name); } set { m_Name = value; } // 关键字'value' 总是代表新值 } public Int32 Age { get { return (m_Age); } set { if (value <= 0) // 关键字'value' 总是代表新值 throw new ArgumentOutOfRangeException("value", "must be >0"); m_Age = value; } } }
于是就可以这样调用:
Employee emp = new Employee(); emp.Name = "Jeffrey Richter"; emp.Age = 45; Console.WriteLine("Employee info: Name = {0}, Age = {1}", emp.Name, emp.Age);
可将属性想象成智能字段,即背后有额外逻辑的字段。CLR支持静态、实例、抽象和虚属性。属性可以使用任意可访问性修饰符修饰。
每个属性都有一个名称和一个类型(类型不能为void).属性不能重载。也就是说不能定义名称相同,类型不同的属性。定义属性时,通常要同时指定get和set两个方法。但是,可以省略set方法来定义一个只读属性,或者省略get方法定义一个只写属性。
定义属性时,取决属性的定义,编译器在最后的托管程序集中生成以下两项或三项:
*)代表属性的get访问器方法的一个方法。仅在属性定义了get访问器方法时生成。
*)代表属性的set访问器方法的一个方法。仅在属性定义了set访问器方法时生成。
*)托管程序集元数据中的一个属性定义。这一项是肯定要生成的。
以前面的Employee类为例,编译器将会生成4个方法定义。如图:
或
编译器在你指定的属性名之前会附加get_或set_前缀,从而自动生成这些方法的名称。C#内建了对属性的支持。当编译器发现代码试图获取或设置一个属性时,它实际会生成对上述某个方法的一个调用。
1. 自动实现属性
如果只是为了封装一个私有字段而创建一个属性,C#还提供了一种更简单的语法,称为自动实现的属性(Automatically implemented Property,AIP)。下面是Name属性的一个例子:
private sealed class Employee { //这是一个自动实现属性 public String Name {get;set;} }
2.合理定义属性
属性和字段的比较:
1)属性可以是只读或只写的,字段访问确总是可读和可写。如果定义一个属性,最好同时为它提供get和set访问器方法。
2)一个属性方法可能抛出异常;字段访问永远不会抛出异常。
3)属性不能作为out或ref参数传给方法;字段却可以。
4)属性方法可能花费较长时间执行;字段的访问总是立即完成的。
5)如果连续多次调用,属性方法每次都可能返回一个不同的值;而字段每次调用都返回相同的值。
6)属性方法可能造成明显的side effect(指访问属性时,除了单纯的设置或获取属性,还会造成对象状态的改变);字段访问永远不会。
7)属性方法可能需要额外的内存,或者返回一个不正确的引用,指向不属于对象状态一部分的某个东西,这样一来,对返回对象的修改就作用不到原始对象身上了。相反,查询字段返回的总是正确的引用,它指向的东西保证是原始对象状态的一部分。
现在的开发人员对属性的依赖有过之而无不及,经常有没有必要都使用属性,仔细看下上面的比较,你会发现在极少数的情况下,才有必要定义属性。属性的唯一好处就是提供了简化的语法,和调用普通方法相比,属性不仅不会提高代码性能,还会妨碍对代码的理解。建议就是让开发人员老老实实的写Getxxx和Setxxx方法,希望编译器提供一种特殊的,简化的,有别于字段访问的语法,是开发人员知道他们实际上是在调用一个方法。
3.对象和集合初始化器
我们经常要构造一个对象,然后设置对象的一些公共属性(或字段)。为了简化这个常见的编程模式,C#语言支持一种特殊的对象初始化语法。比如:[()]代表"()"可要可不要。
Employee e = new Employee[()] { Name = "Jeff", Age = 45 }
对象初始化器语法真正的好处在于,它允许在表达式的上下文(相对于语句的上下文)中编码,允许组合多个函数,进而增强了代码的可读性。于是,就可以这么写了:
string s = new Employee() {Name = "Jeff", Age = 45}.ToString().ToUpper();
4.匿名类型
利用C#的匿名类型,可以使用非常简洁的语法来声明一个不可变的元组类型。元组(Tuple)类型是含有一组属性的类型,这些属性通常以某种方式相互关联。
//定义一个类型,后再它的一个实例,并初始化它的属性 var o1 = new { Name = "Jeff", Year = 1964 }; Console.WriteLine("Name={0}, Year={1}", o1.Name, o1.Year);
第一行代码创建了一个匿名类型,没有在new 关键字后制定类型名称,所以编译器会为我自动创建一个类型名称,而且不会告诉我这个名称是什么(这正是匿名类型一词的由来),但编译器是知道的。虽然我不知道变量o1声明的是什么类型,但可以利用C#的"隐式推断类型局部变量"功能(var)。
编译器支持用另外两种语法声明匿名类型中的属性,它根据变量推断出属性名和类型:
String Name = "Grant"; DateTime dt = DateTime.Now; // 有两个属性的一个匿名类型 // 1. String Name 属性设为"Grant" // 2. Int32 Year 属性设为dt中的年份 var o2 = new { Name, dt.Year };
在这个例子中,编译器判断第一个属性名为Name。由于Name是一个局部变量的名称,所以编译器将属性类型设为与局部变量相同的类型:String。对于第二个属性,编译器使用字段/属性的名称:Year。Year是DateTime类的一个Int32属性,所以匿名类型中的Year属性也是一个Int32。
如果编译器看见你在源代码中定义了多个匿名类型,而且这些类型具有相同的结构,那么它只会创建一个匿名类型定义,但可以创建该类型的多个实例。相同的结构,指在这些匿名类型中,每个属性都有相同的类型和名称,而且这些属性的指定顺序相同。
匿名类型经常和LINQ技术配合使用。可用LINQ进行查询,从而生成由一组对象构成的集合,这些对象都是相同的匿名类型。然后,可以对结果集中的对象进行处理。所有的这些都在一个方法中完成。
5.System.Tuple类型
在System命名空间,Microsoft定义了几个泛型Tuple(元组)类型。它们全部从Object派生,区别只在于元数(泛型参数的个数)。
在计算机编程中,一个函数或运算的元数是指函数获取的实参或操作数的个数。
//这是最简单的 public class Tuple<T1> { private T1 m_item1; public Tuple(T1 item1) { m_Item1 = item1;} public item1 { get { retuen m_Item1; } } }
和匿名类型相似,一旦创建好了一个Tuple,他就不可变了(所有属性都只读)。Tuple类还提供了CompareTo,Equals,GetHashCode和ToString方法,另外还提供了一个Size属性。除此之外,所有Tuple类型都实现了IstruralEquatable,IstructuralComparable和IComparable接口,所以可以比较两个Tuple对象。
二、有参属性
编程语言还支持所谓的有参属性,它的get访问器方法接收一个或多个属性,set访问器方法接收两个或多个参数。C#语言把它们称为索引器。VB称为默认属性。
C#使用数组风格的语法来公开有参属性(索引器)。换句话说,可将索引器看作C#开发人员重载[]操作符的一种方式。下面是一个实例BitArray类,它允许用数组风格的语法来索引由该类的一个实例维护的一组二进制位。
internal sealed class BitArray { // 容纳了二进制位的私有字节数组 private Byte[] m_byteArray; private Int32 m_numBits; // 下面的构造器用于分配字节数组,并将所有位设为 0 public BitArray(Int32 numBits) { // 先验证实参 if (numBits <= 0) throw new ArgumentOutOfRangeException("numBits must be > 0"); // 保留位的个数 m_numBits = numBits; // 为位数组分配字节 m_byteArray = new Byte[(m_numBits + 7) / 8]; } // 下面是索引器(有参属性) public Boolean this[Int32 bitPos] { // 下面是索引器的get访问器方法 get { // 先验证实参 if ((bitPos < 0) || (bitPos >= m_numBits)) throw new ArgumentOutOfRangeException("bitPos", "bitPos must be between 0 and " + m_numBits); // 返回指定索引处的位的状态 return ((m_byteArray[bitPos / 8] & (1 << (bitPos % 8))) != 0); } // 下面是索引器的set访问器方法 set { if ((bitPos < 0) || (bitPos >= m_numBits)) throw new ArgumentOutOfRangeException("bitPos", "bitPos must be between 0 and " + m_numBits); if (value) { // 将指定索引处的位设为true m_byteArray[bitPos / 8] = (Byte) (m_byteArray[bitPos / 8] | (1 << (bitPos % 8))); } else { // 将指定索引处的位设为false m_byteArray[bitPos / 8] = (Byte) (m_byteArray[bitPos / 8] & ~(1 << (bitPos % 8))); } } } }
BitArray类的调用也非常简单:
private static void BitArrayTest() { // 分配含有14个位的bitArray数组 BitArray ba = new BitArray(14); // 调用set访问器方法,将编号为偶数的所有为设为true for (Int32 x = 0; x < 14; x++) { ba[x] = (x % 2 == 0); } // 调用get访问器方法显示所有为的状态 for (Int32 x = 0; x < 14; x++) { Console.WriteLine("Bit " + x + " is " + (ba[x] ? "On" : "Off")); } }
CLR本身并不区分无参属性和有参属性。对CLR来说,每个属性都只是类型中定义的一对方法和一些元数据。将this[...]作为表达一个索引器的语法,这纯粹是C#团队自己的选择,正因如此,C#只允许在对象的实例上定义索引器,C#没有提供定义静态索引器属性的语法,虽然CLR是支持静态有参属性的。
三、调用属性访问器方法时的性能
对于简单的get和set访问器方法,JIT编译器会将代码内联。这样一来,使用属性(而不使用字段)就没有性能上的损失。
内联是指将一个方法的代码直接编译到它的方法中。这样能避免在运行时发出调用所产生的开销,代价是编译好的方法的额代码会变得更大。
由于属性访问器方法通常只包含及少量代码,所以对它们进行内联,反而会使最终生成的本地代码更小,执行更快。
JIT编译器在调试代码时不会内联属性,因为这会变得难以调试。
四、属性访问器的可访问性
我们有时希望为get访问器方法指定一种可访问性,为set访问器方法指定另一种可访问性。如下:
public class SomeType { private String m_name; public String Name { get { return m_name;} protected set { m_name = value;} } }
定义一个属性时,如果两个访问器方法需要具有不同的可访问性,C#语法要求必须为属性本身指定限制最不大的那一种可访问性。然后,在两个访问器中,只能选择一个来应用限制较大的那一种可访问性。如前面例子中,属性本身声明为public,set访问器方法声明为protected(限制比public大)。
五、泛型属性访问器方法
既然属性本质是方法,而且C#和CLR允许方法是泛型的,但是C#不允许定义泛型属性。这从概念上讲不通。属性本用来表示一项可供查询的或设置的对象特征。从概念上讲,属性是不具有行为的。